Merge branch 'integration' into networking-101-udp
@@ -37,7 +37,7 @@ It's going to be a long article, so please feel free to take breaks, grab a drin
|
||||
|
||||
Sound like a fun time? Let's goooo! 🏃🌈
|
||||
|
||||
> The contents of this post was also presented in a talk under the same name. You can [find the slides here](./slides.pptx).
|
||||
> The contents of this post was also presented in a talk under the same name. You can [find the slides here](./slides.pptx) or a live recording of that talk given by the post's author [on our YouTube channel](https://www.youtube.com/watch?v=7AilTMFPxqQ).
|
||||
|
||||
# Introduction To Templates {#intro}
|
||||
|
||||
@@ -436,8 +436,6 @@ _The browser takes the items that've been defined in HTML and turns them into a
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
This tree tells the browser where to place items and includes some logic when combined with CSS, even. For example, when the following CSS is applied to the `index.html` file:
|
||||
|
||||
```css
|
||||
@@ -452,6 +450,7 @@ It finds the element with the ID of `b`, then the children of that tag are color
|
||||
|
||||
> The `ul` element is marked as green just to showcase that it is the element being marked by the first part of the selector
|
||||
|
||||
> If you want to have a better grasp on the DOM and how it relates to the content you see on-screen, [check out our article that outlines what the DOM is and how your code interfaces with it through the browser](/posts/understanding-the-dom/).
|
||||
|
||||
## View Hierarchy Tree
|
||||
|
||||
@@ -1617,9 +1616,9 @@ Of course, this means that you can send any value as the context. Change the cod
|
||||
|
||||
```typescript
|
||||
{
|
||||
$implicit: pigLatinVal,
|
||||
original: this.makePiglatin,
|
||||
makePiglatinCasing: 'See? Any value'
|
||||
$implicit: pigLatinVal,
|
||||
original: this.makePiglatin,
|
||||
makePiglatinCasing: 'See? Any value'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 41 KiB |
BIN
content/blog/basic-overview-of-packets-and-osi/bus_animation.mp4
Normal file
|
After Width: | Height: | Size: 48 KiB |
133
content/blog/basic-overview-of-packets-and-osi/index.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
{
|
||||
title: "Networking 101: A Basic Overview of Packets and OSI",
|
||||
description: 'You use networking every day - even to read this text! Join us as we dive into explaining how we send data across a network and what the OSI model is.',
|
||||
published: '2020-03-11T13:45:00.284Z',
|
||||
authors: ['reikaze', 'crutchcorn'],
|
||||
tags: ['networking', 'osi model'],
|
||||
attached: [],
|
||||
license: 'cc-by-nc-sa-4'
|
||||
}
|
||||
---
|
||||
|
||||
Networking is the foundation that all interactions on the internet are built upon. Every chat you send, every website you visit, _every interaction that is digitally shared with another person is built upon the same fundamentals_. These fundamentals layout and explain how computers are able to communicate with one another and how they interlink with one another in order to provide you with the experience you've come to know and love with the internet.
|
||||
_This article is the beginning of a series that will outline the core components that construct those very same fundamentals_. However, without a holistic view, it may be difficult to follow along with many of the more minute details. As such, we'll be covering what each of the core components at play is and how they fit into the grand scheme of things. The articles to follow will explain and expand upon this base of knowledge.
|
||||
|
||||
It's important to note that _"networking" is a broad, catch-all term that infers **any** communication between one-or-more computer devices_. This includes parts in your computer communicating between themselves. For example, do you need your computer keyboard input to be processed by your CPU to display data on-screen? That requires the networking to transfer the data to the CPU and from your CPU to your monitor, so on and so forth. As a result, this article will be a bit broad in order to cover not just cross-device hardware, but inter-device communication fundamentals as well. We'll dive deeper into cross-device communication in future articles in the series
|
||||
|
||||
> If you don't understand how CPUs work, that's okay, it's not required knowledge for this post. Just know that they take in binary data, process it, then send data out to other devices to use. They convert binary data into binary instructions for other devices to follow
|
||||
>
|
||||
> That said, you need the right binary data to be input into the CPU for it to process, just like our brains need the right input to find the answer of what to do. Because of this, communication with the CPU is integral
|
||||
|
||||
# Architecture {#network-architectures}
|
||||
There are a lot of ways that information can be connected and transferred. We use various types of architecture to connect them.
|
||||
|
||||
_Computers speak in `1`s and `0`s, known as binary_. These binary values come in incredibly long strings of combinations of one of the two symbols to _construct all of the data used in communication_.
|
||||
|
||||
> [We covered how these two numbers can be combined to turn into other data in another post](/posts/non-decimal-numbers-in-tech/). For a better understanding of how binary represents data, check out that post.
|
||||
|
||||
This is true regardless of the architecture used to send data - it’s all binary under-the-hood somewhere in the process. The architecture used to send data is simply a way of organizing the ones and zeros effectively to enable the types of communication required for a specific use-case.
|
||||
|
||||
## Bus Architecture {#bus-architecture}
|
||||
|
||||
For example, one of the ways that we can send and receive data is by, well, sending them. _The bus architecture_, often used in low-level hardware such as CPU inter-communication, _simply streams the ones and zeros directly_.
|
||||
|
||||
It doesn't wait for a full message to be sent or provide many guidelines for how to send the data, it just tells them to "come on over".
|
||||
|
||||
`video: title: "A series of cars driving across 3 lines at various speeds. This is meant to represent the data flow of binary data on a bus architecture": ./bus_animation.mp4`
|
||||
|
||||
In this example, the bus icons are similar to binary data - either a one or a zero. They're able to _move as quickly as possible down a "lane" that is allocated for a specific stream of data to come through_. A collection of "lanes" is called a "bus" (which is where the name of the architecture comes from. I was just being silly by representing the binary data as buses in the video above). Your system, right now, is streaming through _**many**_ thousands of these busses to communicate between your CPU and I/O devices (like your keyboard or speakers) and tons of other things. They're _typically divided to send specific data through specific lanes (or busses)_, but outside of that, there's little high-level organization or concepts to think through.
|
||||
|
||||
Furthermore, because error-handled bi-directional cancelable subscriptions (like the ones you make to servers to connect to the internet) are difficult using the bus architecture, _we typically don't use it for large-scale multi-device networks like the internet_.
|
||||
|
||||
## Packet Architecture {#packet-architecture}
|
||||
|
||||
The weaknesses of the bus architecture led to the creation of the packet architecture. The packet architecture requires a bit more of a higher-level understanding of how data is sent and received. To explain this concept, we'll use an analogy that fits really well.
|
||||
|
||||
Let's say you want to send a note to your friend that's hours away from you. You don't have the internet so you decide to send a letter. In a typical correspondence, you'd send off a letter, include a return address, and wait for a response back. That said, _there's nothing stopping someone from sending more than a single letter before receiving a response_. This chart is a good example of that:
|
||||
|
||||

|
||||
|
||||
Similarly, a packet is _sent from a single sender, received by a single recipient, addressed where to go, and contains a set of information_.
|
||||
|
||||
### Metadata {#packet-metadata}
|
||||
|
||||
Letters may not give you the same kind of continuous stream of consciousness as in-person communications, but they do provide something in return: structure.
|
||||
|
||||
The way you might structure your thoughts when speaking is significantly different from how you might organize your thoughts on paper. For example, in this article, there is a clear beginning, end, and structured headings to each of the items in this article. Such verbose metadata (such as overall length) cannot be communicated via in-person talking. _The way you may structure data in a packet may also differ from how you might communicate data via a bus_.
|
||||
|
||||
That said, simply because there's a defined start and an end does not mean that you cannot _send large sequences of data through multiple packets and stitch them together_. Neither is true for the written word. This article does not contain the full set of information the series we hope to share, but rather provides a baseline and structure for how the rest of the information is to be consumed. So too can packets provide addendums to other packets, if you so wish.
|
||||
|
||||
#### Headers
|
||||
|
||||
Not only does the spoken-word lack the same form of structure that can be provided by the written word, but _you're also able to categorize and assign metadata to a letter_ that you wouldn't be able to do with a conversation. You do this every time you send a letter to someone through the mail: You include their name, address, and return address on the envelope that's used to send the letter. The same is done with packets.
|
||||
|
||||
While the "body" of your packet would contain the data you want the other party to receive, the "header" of the packet might contain data **about** said data. Such metadata might include the size of the contained data or the format that data is in.
|
||||
|
||||

|
||||
|
||||
As a result, you might have a middleware packet handler that reads only the header of the packet in order to decide where to send the packet in question - much like the mail service you use will read the outside of the envelope to see where to send your letter
|
||||
|
||||
`video: title: "An example of a small packet being sent to a small file server and a larger packet being sent to the large file server based on the data in the packet header": ./header_routing.mp4`
|
||||
|
||||
# [It Takes A Village](https://en.wikipedia.org/wiki/It_takes_a_village) To Send A Letter {#osi-layers}
|
||||
|
||||
Understanding what a letter is likely the most important part of the communication aspect if you intend to write letters, but if someone asked you to deliver a letter it helps to have a broader understanding of how the letter gets sent. That's right: _there's a whole structure set in place to send the letters (packets) you want to be sent_. This structure is comprised of many levels, which we'll outline here.
|
||||
|
||||
> For each of these levels, there are many intricacies that we won't be touching on. This is for the sake of conciseness. As this article is only the start of the series, you can expect these intricacies to be explained with greater detail as these articles are released
|
||||
|
||||
This structure is comprised of seven levels for most networking applications. _These layers interact with one another as a form of stack-the-blocks method_. For example, describing layer 4 encapsulates the behavior of layers 3 and 2. As a result, lower-level applications of networking may have fewer than seven layers. That said, those seven levels are, in order:
|
||||
|
||||
- Application
|
||||
- Presentation
|
||||
- Session
|
||||
- Transport
|
||||
- Network
|
||||
- Data Link
|
||||
- Physical
|
||||
|
||||
As you communicate with others online, and as computers communicate within themselves using packets, they go down those layers, then back up them to be processed and interpreted
|
||||
|
||||

|
||||
|
||||
This breakdown of layers is referred to as the [OSI model](https://en.wikipedia.org/wiki/OSI_model). This conceptual model allows us to think abstractly about the different components that make up our network communications. While we won't do a deep-dive into each layer here, we'll try to at least make them fit into our mailroom analogy.
|
||||
|
||||
Let's start from the bottom and make our way up. Remember that each of these layers builds on top of each other, allowing you to make more complex but efficient processes to send data on each step.
|
||||
|
||||
## Physical {#osi-layer-1-physical}
|
||||
|
||||
The physical layer is similar to the trucks, roads, and workers that are driving to send the data. Sure, you could send a letter just by handing letters one-by-one from driver to driver, but without some organization that's usually dispatched to higher levels, things can go wrong (as they often do [in a game of telephone](https://en.wikipedia.org/wiki/Chinese_whispers)).
|
||||
|
||||
In the technical world, _this layer refers to the binary bits themselves_ ([which compose to makeup letters and the rest of structure to your data](/posts/non-decimal-numbers-in-tech/)) _and the physical wiring_ constructed to transfer those bits. As it is with the mail world, this layer alone _can_ be used alone, but often needs delegation from higher layers to be more effective.
|
||||
|
||||
## Data Link {#osi-layer-2-data-link}
|
||||
|
||||
Data link would be like UPS or FedEx offices: sending information between post office to post office. These offices don't have mail sorters yet (that's a layer up) but they do provide a means for drivers to arrive to exchange mail at a designated area. As a result, instead of having to meet the drivers in the road to receive my mail, I can simply go to a designated office to receive my mail.
|
||||
|
||||
Likewise, _the data link layer is the layer that transfers binary data between different locations_. This becomes especially helpful when _dealing with local networks that only exchange data between a single physical location_, where you might not need the added complexity large-level packet sorting might come into play.
|
||||
|
||||
## Network {#osi-layer-3-network}
|
||||
|
||||
The network layer is similar to the mail sorters. Between being transferred from place to place, there may be instances where the mail is needed to be sorted and organized. This is _done with packets in the network layer to handle routing_ and other related activities between clients
|
||||
|
||||
## Transport {#osi-layer-4-transport}
|
||||
|
||||
The transport layer delivers it from the post office to my apartment building. This means that not only does the package gets delivered from post-office building to post-office building, but it gets to-and-from its destination as intended.
|
||||
|
||||
## Session {#osi-layer-5-session}
|
||||
|
||||
With newer packages delivered through services like UPS, you may want a tracking number for your package. This is similar to the session layer. With this layer, it includes a back-and-forth that can give you insight into the progress of the delivery or even include information like return-to-sender.
|
||||
|
||||
## Presentation {#osi-layer-6-presentation}
|
||||
|
||||
But when a package gets received by you, it doesn't stop there, does it? You want to bring the package inside your home. For most packages, this is relatively trivial - you simply take it inside. However, for some specialized instances, this may require hiring movers to get a couch in your house. In this same way, HTTP and other protocols don't typically differentiate between the presentation layer and the application layer, but some networks do. When they do, they use the presentation layer to outline how the data is formed for sending and receiving
|
||||
|
||||
## Application {#osi-layer-7-application}
|
||||
|
||||
You've just been delivered the fancy new blender you ordered for smoothies. After unwrapping the package, you plug it in and give it a whirl, making the most delicious lunch-time smoothie you've ever had. Congrats, you've just exemplified the application layer. In this layer, it encapsulates the layer your user (developer or end-user alike) will use, the application that communicates back-and-forth and the reason you wanted to send data in the first place.
|
||||
|
||||
# Conclusion
|
||||
|
||||
We've done an initial overview of what layers you utilize when accessing a network. Although we've only done an initial glance at those layers, the next few steps will outline what those layers comprise of, and how data can transfer across a network. These steps will come in the order of various articles in the series in the future. To make sure you don't miss those next articles, be sure to subscribe to our newsletter down below!
|
||||
|
||||
You can also [join us in our community Discord](https://discord.gg/FMcvc6T) and chat with us there!
|
||||
36
content/blog/basic-overview-of-packets-and-osi/osi_layer.svg
Normal file
|
After Width: | Height: | Size: 714 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 406 KiB |
309
content/blog/debugging-nodejs-programs-using-chrome/index.md
Normal file
@@ -0,0 +1,309 @@
|
||||
---
|
||||
{
|
||||
title: "Debugging NodeJS Applications Using Chrome",
|
||||
description: 'Learn how to interactively debug your NodeJS applications using a GUI-based debugger built into Chrome.',
|
||||
published: '2020-01-21T05:12:03.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['nodejs', 'debugging', 'chrome'],
|
||||
attached: [],
|
||||
license: 'cc-by-nc-sa-4'
|
||||
}
|
||||
---
|
||||
|
||||
Debugging is one of the most difficult aspects of development. Regardless of skill level, experience, or general knowledge, every developer finds themselves in an instance where they need to drop down and start walking through the process. Many, especially those who are in complex environments or just starting on their developmental path, may utilize `console.log`s to help debug JavaScript applications. However, there is a tool for developers using Node.JS that makes debugging significantly easier in many instances.
|
||||
|
||||
The tool I'm referring to is [the Node Debugger utility](https://nodejs.org/api/debugger.html). While this utility is powerful and helpful all on its own, _it can be made even more powerful by utilizing the power of the Chrome debugger_ to attach to a Node debuggable process in order to _provide you a GUI for a debugging mode_ in your Node.JS applications.
|
||||
|
||||
Let's look at how we can do so and how to use the Chrome debugger for such purposes.
|
||||
|
||||
# Example Application {#example-code}
|
||||
|
||||
Let's assume we're building an [Express server](https://expressjs.com/) in NodeJS. We want to `GET` an external endpoint and process that data, but we're having issues with the output data. Since it's not clear where the issue resides, we decide to turn to the debugger.
|
||||
|
||||
Let's use the following code as our example Express app:
|
||||
|
||||
```javascript
|
||||
// app.js
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
const request = require("request");
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
request("http://www.mocky.io/v2/5e1a9abe3100004e004f316b", (error, response, body) => {
|
||||
const responseList = JSON.parse(body);
|
||||
const partialList = responseList.slice(0, 20);
|
||||
const employeeAges = partialList.map(employee => {
|
||||
return employee.employeeAge;
|
||||
});
|
||||
console.log(employeeAges);
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
You'll notice that we're using the dummy endpoint http://www.mocky.io/v2/5e1a9abe3100004e004f316b. This endpoint returns an array of values with a shape much like this:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"employee_name": "Adam",
|
||||
"employee_salary": "12322",
|
||||
"employee_age": "23",
|
||||
"profile_image": ""
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Once you run the `app.js` file in Node, however, you'll see the `console.log`s of:
|
||||
|
||||
```javascript
|
||||
[ undefined ]
|
||||
```
|
||||
|
||||
Instead of the ages of the employees as we might expect. We'll need to dive deeper to figure out what's going on, let's move forward with setting up and using the debugger.
|
||||
|
||||
> You may have already spotted the error in this small code sample, but I'd still suggest you read on. Having the skillsets to run a debugger can help immeasurably when dealing with large-scale codebases with many moving parts or even when dealing with an unfamiliar or poorly documented API.
|
||||
|
||||
# Starting the Debugger {#starting-the-debugger}
|
||||
|
||||
Whereas a typical Express application might have `package.json` file that looks something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "example-express-debug-code",
|
||||
"version": "1.0.0",
|
||||
"main": "app.js",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"request": "^2.88.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./app.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We'll be adding one more `scripts` item for debug mode:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "example-express-debug-code",
|
||||
"version": "1.0.0",
|
||||
"main": "app.js",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"request": "^2.88.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./app.js",
|
||||
"debug": "node --inspect ./app.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once you add in this flag, you can simply run `npm run debug` to start the debuggable session.
|
||||
|
||||
> A quick sidenote:
|
||||
> Some folks like to use [the `nodemon` tool](https://nodemon.io/) in order to get their application to reload upon making changes to their source file.
|
||||
> That doesn't mean you can't join in the debugger fun! Just replace `node` with `nodemon` for the following `package.json`:
|
||||
>
|
||||
> ```json
|
||||
> {
|
||||
> "name": "example-express-debug-code",
|
||||
> "version": "1.0.0",
|
||||
> "main": "app.js",
|
||||
> "dependencies": {
|
||||
> "express": "^4.17.1",
|
||||
> "request": "^2.88.0"
|
||||
> },
|
||||
> "devDependencies": {
|
||||
> "nodemon": "^2.0.2"
|
||||
> },
|
||||
> "scripts": {
|
||||
> "start": "nodemon ./app.js",
|
||||
> "debug": "nodemon --inspect ./app.js"
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
|
||||
Once you start your debuggable session, you should be left with a message similar to the following:
|
||||
|
||||
```
|
||||
Debugger listening on ws://127.0.0.1:9229/ffffffff-ffff-ffff-ffff-ffffffffffff
|
||||
For help, see: https://nodejs.org/en/docs/inspector
|
||||
```
|
||||
|
||||
At this point, _it will hang and not process the code or run it_. That's okay though, as we'll be running the inspector to get the code to run again in the next step.
|
||||
|
||||
# The Debugger {#the-debugger}
|
||||
|
||||
In order to access the debugger, you'll need to open up Chrome and go to the URL `chrome://inspect`. You should see a list of selectable debug devices, including the node instance you just started.
|
||||
|
||||

|
||||
|
||||
Then you'll want to select `inspect` on the node instance.
|
||||
|
||||
Doing so will bring up a screen of your entrypoint file with the source code in a window with line numbers.
|
||||
|
||||

|
||||
|
||||
These line numbers are important for a simple reason: They allow you to add breakpoints. In order to explain breakpoints, allow me to make an analogy about debug mode to race-car driving.
|
||||
|
||||
Think about running your code like driving an experimental race-car. This car has the ability to drive around the track, you can watch it run using binoculars, but that doesn't give you great insight as to whether there's anything wrong with the car. If you want to take a closer inspection of a race-car, you need to have it pull out to the pit-stop in order to examine the technical aspects of the car before sending it off to drive again.
|
||||
|
||||
It's similar to a debug mode of your program. You can evaluate data using `console.log`, but _to gain greater insight, you may want to pause your application_, inspect the small details in the code during a specific state, and to do so you must pause your code. This is where breakpoints come into play: they allow you to place "pause" points into your code so that when you reach the part of code that a breakpoint is present on, your code will pause and you'll be given much better insight to what your code is doing.
|
||||
|
||||
To set a breakpoint from within the debugging screen, you'll want to select a code line number off to the left side of the screen. This will create a blue arrow on the line number.
|
||||
|
||||
> If you accidentally select a line you didn't mean to, that's okay. Pressing this line again will disable the breakpoint you just created
|
||||
|
||||

|
||||
|
||||
A race-car needs to drive around the track until the point where the pit-stop is in order to be inspected; _your code needs to run through a breakpoint in order to pause and give you the expected debugging experience_. This means that, with only the above breakpoint enabled, the code will not enter into debug mode until you access `localhost:3000` in your browser to run the `app.get('/')` route.
|
||||
|
||||
> Some applications may be a bit [quick-on-the-draw](https://en.wiktionary.org/wiki/quick_on_the_draw) in regards to finding an acceptable place to put a breakpoint. If you're having difficulties beating your code running, feel free to replace the `--inspect` flag with `--inspect-brk` which will automatically add in a breakpoint to the first line of code in your running file.
|
||||
>
|
||||
> This way, you should have the margins to add in a breakpoint where you'd like one beforehand.
|
||||
|
||||
# Using The Debugging Tools {#using-debug-tools}
|
||||
|
||||
Once your code runs through a breakpoint, this window should immediately raise to focus (even if it's in the background).
|
||||
|
||||

|
||||
|
||||
> If you don't see the `Console` tab at the bottom of the screen, as is shown here, you can bring it up by pressing the `Esc` key. This will allow you to interact with your code in various ways that are outlined below.
|
||||
|
||||
Once you do so, you're in full control of your code and its state. You can:
|
||||
|
||||
- _Inspect the values of variables_ (either by highlighting the variable you're interested in, looking under the "scope" tab on the right sidebar, or using the `Console` tab to run inspection commands à la [`console.table`](https://developer.mozilla.org/en-US/docs/Web/API/Console/table) or [`console.log`](https://developer.mozilla.org/en-US/docs/Web/API/Console/log)):
|
||||
|
||||

|
||||
- _Change the value of a variable:_
|
||||

|
||||
- _Run arbitrary JavaScript commands_, similar to how a code playground might allow you to:
|
||||

|
||||
|
||||
## Running Through Lines {#running-through-lines}
|
||||
|
||||
But that's not all! Once you make changes (or inspect the values), you're also able to control the flow of your application. For example, you may have noticed the following buttons in the debug window:
|
||||
|
||||

|
||||
|
||||
Both of these buttons allow you to control where your debugger moves next. _The button to the left_ is more of a "play/pause" button. Pressing this _will unpause your code and keep running it_ (with your variable changes intact) _until it hits the next breakpoint_. If this happens to be two lines down, then it will run the line in-between without pausing and then pause once it reached that next breakpoint.
|
||||
|
||||
So, if we want to see what happens after the `body` JSON variable is parsed into a variable, we could press the "next" button to the right to get to that line of code and pause once again.
|
||||
|
||||

|
||||
|
||||
Knowing this, let's move through the next few lines manually by pressing each item. The ran values of the variables as they're assigned should show up to the right of the code itself in a little yellow box; This should help you understand what each line of code is running and returning without `console.log`ging or otherwise manually.
|
||||
|
||||
![A screenshot showing ran lines until line 12 of the "console.log". It shows that "employeeAges" is "[undefined]"](./next_few_lines.png)
|
||||
|
||||
But oh no! You can see, `employeeAges` on line `9` is the one that results in the unintended `[undefined]`. It seems to be occurring during the `map` phase, so let's add in a breakpoint to line `10` and reload the `localhost:3000` page (to re-run the function in question).
|
||||
|
||||
Once you hit the first breakpoint on line `7`, you can press "play" once again to keep running until it hits the breakpoint on line `10`.
|
||||
|
||||

|
||||
|
||||
This will allow us to see the value of `employee` to see what's going wrong in our application to cause an `undefined` value.
|
||||
|
||||

|
||||
|
||||
Oh! As we can see, the name of the field we're trying to query is `employee_age`, not the mistakenly typo'd `employeeAge` property name we're currently using in our code. Let's stop our server, make the required changes, and then restart the application.
|
||||
|
||||
We will have to run through the breakpoints we've set by pressing the "play" button twice once our code is paused, but once we get through them we should see something like the following:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
There we go! We're able to get the expected "23"! That said, it was annoying to have to press "play" twice. Maybe there's something else we can do in similar scenarios like this?
|
||||
|
||||
## Disabling Breakpoints {#disabling-breakpoints}
|
||||
|
||||
As mentioned previously in an aside, you can disable breakpoints as simply as pressing the created breakpoint once again (pressing the line number will cause the blue arrow to disappear). However, you're also able to temporarily disable all breakpoints if you want to allow code to run normally for a time. To do this, you'll want to look in the same toolbar as the "play" and "skip" button. Pressing this button will toggle breakpoints from enabling or not. If breakpoints are disabled, you'll see that the blue color in the arrows next to the line number will become a lighter shade.
|
||||
|
||||

|
||||
|
||||
Whereas code used to pause when reaching breakpoints, it will now ignore your custom set breakpoints and keep running as normal.
|
||||
|
||||
## Step Into {#debugger-step-into}
|
||||
|
||||
In many instances (such as the `map` we use in the following code), you may find yourself wanting to step _into_ a callback function (or an otherwise present function) rather than step over it. For example, [when pressing the "next" button in the previous section](#running-through-lines), it skipped over the `map` instead of running the line in it (line 10). This is because the arrow function that's created and passed to `map` is considered its own level of code. To dive deeper into the layers of code and therefore **into** that line of code, instead of the "next line" button to advance, you'll need to press the "step into" button.
|
||||
|
||||

|
||||
|
||||
Let's say you're on line `9` and want to move into the `map` function. You can press the "step into" to move into line `10`.
|
||||
|
||||

|
||||
|
||||
Once inside the `map` function, there's even a button _to get you outside of that function and back to the parent caller's next line_. This might if you're inside of a lengthy `map` function, have debugged the line you wanted to inspect, and want to move past the `map` to the next line (the `console.log`). Doing so is as simple as "stepping in" a function, you simply press the "step outside" button to move to the next line
|
||||
|
||||

|
||||
|
||||
> While the example uses a callback in `map`, both of these "step into" and "step out of" also work on functions that are called. For example, assume the code was written as the following:
|
||||
>
|
||||
> ```javascript
|
||||
> const getEmployeeAges = partialList => {
|
||||
> const ageArray = [];
|
||||
> for (employee of partialList) {
|
||||
> ageArray.push(employee.employee_age);
|
||||
> }
|
||||
> return ageArray;
|
||||
> };
|
||||
>
|
||||
> app.get('/', (req, res) => {
|
||||
> request('http://www.mocky.io/v2/5e1a9abe3100004e004f316b', (error, response, body) => {
|
||||
> const responseList = JSON.parse(body);
|
||||
> const partialList = responseList.slice(0, 20);
|
||||
> const employeeAges = getEmployeeAges(partialList);
|
||||
> console.log(employeeAges);
|
||||
> });
|
||||
> });
|
||||
> ```
|
||||
>
|
||||
> You would still be able to "step into" `getEmployeeAges` and, once inside, "step outside" again in the same manor of the `map` function, as shown prior.
|
||||
|
||||
# Saving Files {#editing-files-in-chrome}
|
||||
|
||||
One more feature I'd like to touch on with the debugger before closing things out is the ability to edit the source files directly from the debugger. Using this feature, it can make the Chrome debugger a form of lite IDE, which may improve your workflow. So, let's revert our code to [the place it was at before we applied the fix we needed](#example-code) and go from there.
|
||||
|
||||

|
||||
|
||||
Once this window is open, you're able to tab into or use your cursor to select within the text container that holds your code. Once inside, it should work just like a `textarea`, which _allows you to change code as you might expect from any other code editor_. Changing line `10` to `return employee.employee_age` instead of `return employee.employeeAge` will show an asterisk (`*`) to let you know your changes have not yet been applied. _Running your code in this state will not reflect the changes made to the code content on the screen_, which may have unintended effects.
|
||||
|
||||

|
||||
|
||||
In order to make your changes persist, you'll need to press `Ctrl + S` or `Command + S` in order to save the file (much like a Word document). Doing so will bring up a yellow triangle instead of an asterisk indicating _your changes are not saved to the source file but your changes will now take effect_. Re-running the `localhost:3000` route will now correct the behavior you want, but if you open `app.js` in a program like Visual Studio Code, it will show the older broken code.
|
||||
|
||||

|
||||
|
||||
Not only does VS Code not recognize your changes, but once you close your debugging window, you won't know what you'd changed in order to get your code to work. While this may help in short debugging sessions, this won't do for a longer session of code changes. To do that, you'll want your changes to save to the local file system.
|
||||
|
||||
## Persisting Changes {#chrome-as-ide-persist-changes}
|
||||
|
||||
In order to save the changes from inside the Chrome to the file system, you need to permit Chrome access to read and write your files. To do this, you'll want to press the "Add folder to workspace" button off to the left of the code screen.
|
||||
|
||||

|
||||
|
||||
Selecting the folder your `app.js` is present in will bring up the dialog to give Chrome permission to view the files and folders within. You'll need to press "Allow" in order to save your saves to your file system.
|
||||
|
||||

|
||||
|
||||
Once done, you should now see a list of the files and folders in the parent folder. It should automatically have highlights over the `app.js` file and remove the yellow triangle in favor of another asterisk.
|
||||
|
||||

|
||||
|
||||
As I'm sure you've guessed, the asterisk indicates that you'll need to save the file again. Once done (using the key combo), the asterisk should disappear.
|
||||
|
||||
It's not just JavaScript files you're able to edit, though! You can click or use your keyboard to navigate the file tree of the parent folder. Doing so will allow you to edit and save changes to _any_ file in the filesystem. This would include the `package.json` file in the folder.
|
||||
|
||||

|
||||
|
||||
# Conclusion
|
||||
|
||||
While we've covered a lot of functionality present within the Chrome debugger, there's still more to cover about it! If you'd like to read more about it, you may want to take a look at [the extensive blog series by the Chrome team](https://developers.google.com/web/tools/chrome-devtools/javascript) that offers a much deeper dive into all of the debugging tools present within Chrome. Luckily, the skills that you gain while debugging Node.JS applications carries over to debugging front-end JavaScript, so hopefully this article has helped introduce you to the myriad of tools that Chrome has to offer.
|
||||
|
||||
Leave a comment down below if you have a question or comment, or feel free to join [our Discord](https://discord.gg/FMcvc6T) to have a direct line to us about the article (or just general tech questions).
|
||||
|
After Width: | Height: | Size: 410 KiB |
|
After Width: | Height: | Size: 489 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 521 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 445 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
@@ -3,6 +3,7 @@
|
||||
title: 'Hard grids & baselines: How I achieved 1:1 fidelity on Android',
|
||||
description: 'Testing the limits of `firstBaselineToTopHeight` and `lastBaselineToBottomHeight` to deliver a perfect result.',
|
||||
published: '2019-10-07T22:07:09.945Z',
|
||||
edited: '2020-02-02T22:07:09.945Z',
|
||||
authors: ['edpratti'],
|
||||
tags: ['android', 'design', 'figma'],
|
||||
attached: [],
|
||||
@@ -43,33 +44,35 @@ Android has two main `TextView`s; one of them is `AppCompatTextView`, which has
|
||||
|
||||
With Android 9.0 Pie, Google introduced 3 new attributes for `TextView`s: `firstBaselineToTopHeight`, `lastBaselineToBottomHeight` and `lineHeight`. These control everything you’d need to build a UI with.
|
||||
|
||||
Shortly after, Google removed those API restrictions by backporting those features to [`AppCompatTextView`](https://developer.android.com/reference/androidx/appcompat/widget/AppCompatTextView) and subsequently, [`MaterialTextView`](https://developer.android.com/reference/com/google/android/material/textview/MaterialTextView?hl=en). This means these attributes can now be used across all supported versions of Android!
|
||||
|
||||
However, if you seek fidelity, you’ll find that `lineHeight` on Android differs from other platforms and most design tools.
|
||||
|
||||
## How is it any different?
|
||||
|
||||
Let us take a look at some examples; one with a single line, then two lines, then three lines with line height set to `24pt/sp`.
|
||||
|
||||

|
||||

|
||||
|
||||
As you can probably tell, Android `TextViews` are always smaller than the ones given to a developer from a design tool and those implemented on the web. In reality, Android’s `lineHeight` is not line-height at all! **It’s just a smart version of line-spacing.**
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Now you might ask yourself, “*How can I calculate the height of each `TextView`, then?*”
|
||||
|
||||
When you use a `TextView`, it has one parameter turned on by default: **`includeFontPadding`**. `includeFontPadding` increases the height of a `TextView` to give room to ascenders and descenders that might not fit within the regular bounds.
|
||||
|
||||

|
||||

|
||||
|
||||
Now that we know how Android’s typography works, let’s look at an example.
|
||||
|
||||
Here’s a simple mockup, detailing the spacing between a title and a subtitle. It is built at `1x`, with Figma, meaning line height defines the final height of a text box — not the text size. (This is how most design tools work)
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
*Of course, because it’s Android, the line height has no effect on the height of the `TextView`, and the layout is therefore `8dp` too short of the mockups.*
|
||||
|
||||
@@ -79,7 +82,7 @@ But even if it did have an effect, the problems wouldn’t stop there; the issue
|
||||
|
||||
Designers, like myself, like to see perfect alignment. We like consistent values and visual rhythm.
|
||||
|
||||

|
||||

|
||||
|
||||
Unfortunately, translating values from a design tool wasn’t possible. You had the option to either pixel nudge (pictured above, right), or forget about alignment altogether, thus leading to an incorrect implementation that would, yet again, be shorter than the mockups.
|
||||
|
||||
@@ -87,13 +90,13 @@ Unfortunately, translating values from a design tool wasn’t possible. You had
|
||||
|
||||
_`firstBaselineToTopHeight`_ and _`lastBaselineToBottomHeight`_ are powerful tools for Android design. They do as the name suggests: If _`firstBaselineToTopHeight`_ is set to `56sp`, then that’ll become the distance between the first baseline and the top of a `TextView`.
|
||||
|
||||

|
||||

|
||||
|
||||
This means that designers, alongside developers, can force the bounds of a `TextView` to match the design specs and open the door to perfect implementations of their mockups.
|
||||
|
||||
This is something I’ve personally tested in an app I designed. [**Memoire**, a note-taking app](http://tiny.cc/getmemoire) for Android, is a 1:1 recreation of its mockups — for every single screen. This was made possible due to these APIs — *and because [**@sasikanth**](https://twitter.com/its\_sasikanth) is not confrontational* — since text is what almost always makes baseline alignment and hard grids impossible to implement in production.
|
||||
|
||||
`video: title: "Near-perfect duplication of guidelines against Memoire's mockups and actual app": ./images/Memoire_Bounds_and_Baselines.mp4`
|
||||
`video: title: "Near-perfect duplication of guidelines against Memoire's mockups and actual app": ./memoire_bounds_and_baselines.mp4`
|
||||
|
||||
*Memoire’s TextViews are all customized using these APIs.*
|
||||
|
||||
@@ -101,7 +104,7 @@ This is something I’ve personally tested in an app I designed. [**Memoire**, a
|
||||
|
||||
In reality, the new attributes were actually made to be used when creating layouts: you want to make sure the baseline is a certain distance from another element, and it also helps to align the first and lastBaseline to a `4dp` grid. This mirrors the way iOS layouts are built.
|
||||
|
||||

|
||||

|
||||
|
||||
**However, there’s one giant flaw: You can’t align a `TextView`’s `firstBaseline` to another `TextView`’s `lastBaseline`.** So a problem immediately arises due to this limitation:
|
||||
|
||||
@@ -109,13 +112,13 @@ In reality, the new attributes were actually made to be used when creating layou
|
||||
|
||||
As you might imagine, **if we want to keep our text aligned to a baseline grid, we need to ensure that the height of each `TextView` is a multiple of 4 while doing so.** This means we must apply first and lastBaseline attributes to both / all of the stacked TextViews — and that becomes hard to maintain.
|
||||
|
||||

|
||||

|
||||
|
||||
|✅ Good|🛑 Bad|
|
||||
|--|--|
|
||||
|Applying `firstBaseline` and `lastBaseline` in styles allows you to know exactly what the distance between baselines is, without having to set them one by one to ensure they properly align to a `4dp` grid. | Without applying `firstBaseline` and `lastBaseline` in styles, you can’t detect what the default values are, so you are forced to apply these one by one to every `TextView` to ensure they align to a `4dp` grid. |
|
||||
|
||||
`video: title: "A comparison of how text spacing is applied on iOS and Android": ./images/iOS_vs_Android.mp4`
|
||||
`video: title: "A comparison of how text spacing is applied on iOS and Android": ./ios_vs_android.mp4`
|
||||
|
||||
The solution is to apply them in your `styles.xml` so that, when themed, the `TextView` is given the right text size, height, font, and baseline properties.
|
||||
|
||||
@@ -125,7 +128,7 @@ The solution is to apply them in your `styles.xml` so that, when themed, the `Te
|
||||
|
||||
The overrides will take precedence to whatever value you set in your **`styles.xml`**, requiring you to hunt down occurrences until you can find a layout that was broken due to the change. Let’s look at an example:
|
||||
|
||||
`video: title: "Allowing margin changes instead will let the text grow to it's expected sie without having issues with the baseline not being centered": ./images/Dont_Override.mp4`
|
||||
`video: title: "Allowing margin changes instead will let the text grow to it's expected sie without having issues with the baseline not being centered": ./dont_override.mp4`
|
||||
|
||||
Implementing margins instead of overriding values also matches the way layouts work within Android Studio and design tools like Sketch and Figma. It also ensures that your layouts can scale well to different font sizes.
|
||||
|
||||
@@ -135,7 +138,7 @@ It’s actually pretty simple. Let’s walk through how to adapt one of Material
|
||||
|
||||
**Step 1: Place a text box of the text style you’d like to adapt — in this case, Headline 6.**
|
||||
|
||||

|
||||

|
||||
|
||||
*Text box within Figma.*
|
||||
|
||||
@@ -143,7 +146,7 @@ Here we can see that the text box has a height of `32`. This is inherited from t
|
||||
|
||||
> Headline 6 = `20` (text size) `* 1.33` (`includeFontPadding`) = `26.667sp`
|
||||
|
||||

|
||||

|
||||
|
||||
*`TextView` on Android.*
|
||||
|
||||
@@ -151,27 +154,27 @@ Now resize your Figma text box to `26.6` — *it will round it to `27`, but that
|
||||
|
||||
**Step 2: With the resized text box, align its baseline with the nearest `4dp` breakpoint in your grid.**
|
||||
|
||||

|
||||

|
||||
|
||||
*Baseline now sits on the `4dp` grid.*
|
||||
|
||||
**Step 3: Measure the distance between the baseline and the top and bottom of the text box.**
|
||||
|
||||

|
||||

|
||||
|
||||
*`firstBaselineToTopHeight`: `20.66` | `lastBaselineToBottomHeight`: `6.0`*
|
||||
|
||||
**Step 4: Now right click the text box and select Frame Selection.**
|
||||
|
||||

|
||||

|
||||
|
||||
*When created from an object, a frame’s dimensions are dependent on the content inside it.*
|
||||
|
||||
**Step 5: While holding Ctrl / Command, drag the frame handles and resize it so that the top and bottom align with the nearest baselines beyond the minimum values.**
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
**NOTE: Keep in mind we must not resize the text box with it. Holding Ctrl / Command is very, very important.**
|
||||
|
||||
@@ -181,23 +184,23 @@ The same thing was done to the last baseline and the bottom; we changed it from
|
||||
|
||||
**Step 6: Select the text box inside the frame, and set the text to Grow Vertically.**
|
||||
|
||||

|
||||

|
||||
|
||||
This will cause the text box to return to its original height of `32sp` — inherited from the line height.
|
||||
|
||||

|
||||

|
||||
|
||||
*The text box is 1sp down from the frame, but that’s normal. We no longer care about the text box height.*
|
||||
|
||||
**Step 7: With the text box selected, set its constraints to *Left & Right* and *Top & Bottom*.**
|
||||
|
||||

|
||||

|
||||
|
||||
*Now your text box will resize with your frame. This is essential when using the text components.*
|
||||
|
||||
You would need to find these values for every text style in your app, but if you’re taking the Material Design Type Spec as a base for your own, I have already measured and picked the right values for each! _**Resources at the end.**_
|
||||
|
||||

|
||||

|
||||
|
||||
## How to implement these values (as a developer)
|
||||
|
||||
@@ -224,7 +227,7 @@ We first set up a `TextAppearance` — which your app probably already has —
|
||||
|
||||
Let’s use Memoire once again as an example.
|
||||
|
||||

|
||||

|
||||
|
||||
### Each has a different function:
|
||||
|
||||
@@ -235,7 +238,7 @@ For example, _**`textAppearanceCaption`**_, _**`textAppearanceBody1`**_, etc.
|
||||
|
||||
**`TextStyle`:** Applied to `TextView`s in layouts, to ensure `4dp` alignment.
|
||||
|
||||

|
||||

|
||||
|
||||
*What happens to a `TextView` when a `TextStyle` is properly applied.*
|
||||
|
||||
@@ -249,7 +252,7 @@ When setting a style to a `TextView`, keep in mind that `firstBaseline` and `las
|
||||
|
||||
Applying a `TextStyle` to a component — instead of a `TextAppearance` — causes serious issues.
|
||||
|
||||

|
||||

|
||||
|
||||
*Uh-oh…*
|
||||
|
||||
@@ -261,7 +264,7 @@ As far as other issues, I haven’t been able to find any.
|
||||
|
||||
Now that you’ve scrolled all the way down without reading a single word, here’s all the stuff you’ll need:
|
||||
|
||||

|
||||

|
||||
|
||||
*Figma document with code and layout samples.*
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
163
content/blog/how-to-pick-tech-stacks-for-new-projects/index.md
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
{
|
||||
title: "How to Pick Tech Stacks For New Projects",
|
||||
description: 'I often get asked: "How do you pick a tech stack for your projects?". This article answers that by outlining what questions you should be asking early on',
|
||||
published: '2020-03-02T05:12:03.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['engineering', 'advice'],
|
||||
attached: [],
|
||||
license: 'cc-by-nc-sa-4'
|
||||
}
|
||||
---
|
||||
|
||||
I talk to engineers; I talk to a lot of engineers. I've spoken to engineers from various backgrounds and various skillsets. We all have had to face the same thing at some point: "What tools do you pick for the job?". It's a question that was phrased perfectly by [Lindsay Campbell](https://www.linkedin.com/in/lindsaycampbelldeveloper/) on [the public Unicorn Utterances Discord server our community use to chat](https://discord.gg/FMcvc6T):
|
||||
|
||||
_"When you start a new project, how do you go about planning it? How do you know what features you want? How do you even start do you figure out frameworks, libraries you will use for those different features? What do you do to also make sure that all the different technologies you will be using will work together nicely in your application? Thanks!"_
|
||||
|
||||
> Side note, Lindsay is an excellent engineer. You should [check out her profile](https://www.linkedin.com/in/lindsaycampbelldeveloper/) and give her a follow
|
||||
|
||||
The answer is ironically a lot less about the solution to a given problem as much as it is discovering the root of the problem itself.
|
||||
|
||||
For example, let's look at a project I've been debating on spinning up with a few folks:
|
||||
|
||||
An online-first Bootcamp system with interactive quizzes, live-streamed content (like video sessions), a large set of hosted video, and other education-related features.
|
||||
|
||||
The first thing I do, _before looking at any tech whatsoever is think about it from a business perspective_.
|
||||
|
||||
- "Who is this for?"
|
||||
- "What do they want?"
|
||||
- "What's most important to have done first?"
|
||||
- "What are my stretch goals/holistic vision/defining drive?"
|
||||
- "What is the profit model?" (if that matters to that project)
|
||||
- "What's my budget?" (_Budget means more than just finances_, if you're talking about a side project, the budget is the time you have to work on the project).
|
||||
|
||||
These are all of the questions I layout before even thinking about coding. I first start by white-boarding these things, explaining them to both myself and my partners, and generally doing my due-diligence concerning project planning.
|
||||
|
||||
## Wholistic Vision {#whats-your-vision}
|
||||
|
||||
My holistic vision would consist of:
|
||||
|
||||
- Simple-to-use UI
|
||||
|
||||
- Lots of full-filled content, such as video courses or pictures to serve alongside their written content
|
||||
|
||||
- A single place to host a course for someone
|
||||
- An independent creator feeling comfortable enough to host content here without having to make their landing page in a separate service. As such, we'll need to provide a lightweight customization of a page to showcase their own brand/course.
|
||||
- Focus on groups rather than single courses. Subscribing to a single content group/creator rather than "React course #1" which has no clear distinction from another "React course #1"
|
||||
|
||||
While the first point doesn't inform us of much at this early stage (we'll touch on UI tooling selection later), we can glean from the second point that we'll have to maintain some kind of storage layer. This will be something we'll need to keep in mind as we structure our goals.
|
||||
|
||||
## Target Audience {#who-are-you-targetting}
|
||||
|
||||
In this case, the groups of people I would want to appeal to are:
|
||||
|
||||
- Students looking for a place to learn remotely
|
||||
- Independent teachers looking for a unified platform to publish through
|
||||
- Bootcamps looking to have an organized, content-focused site to host their courses
|
||||
|
||||
This potentially broad appeal might be able to drive a lot of business, but without a focused plan and a solid profit model, the project would fall flat.
|
||||
|
||||
## Profit Model {#layout-your-profit-model}
|
||||
|
||||
We'd plan to drive revenue by using the following profit model:
|
||||
|
||||
- We'd focus on a B2B type solution where you could pay for a pro account that would make promoting your courses and stuff to other students easier.
|
||||
- No students would pay for accounts but might pay for a subscription to course content
|
||||
- We'd likely take a cut of the subscription or charge for course features in some way
|
||||
|
||||
## Budget {#define-your-budget}
|
||||
|
||||
Finally, none of this can be done without resources. These resources should be budgeted upfront, so what have we got? We have:
|
||||
|
||||
- Myself, maybe a few other folks local to my area working on this project
|
||||
- This project would be a second job or side project for all of us
|
||||
- Additionally, I'd be working on this project on top of working on other UU content and other side projects
|
||||
|
||||
Our limited budget tells us that we will have to be hyper-focused when it comes time to planning out our MVP. We'll need to keep our goals well defined, and if we intend to make it profitable, we'll need to _keep those goals closely aligned with our profit model's requirements define_.
|
||||
|
||||
Now that we have a more precise goal of what the problem space we're entering is, we can more clearly define our goals (next part)
|
||||
|
||||
# Goals {#mvp}
|
||||
|
||||
Now that we're onto setting goals, I like to start thinking about "What is the bare minimum we need to show this to someone to spark a conversation." _This is often called the "minimum viable product" or "MVP" for short_.
|
||||
|
||||
Looking at what we need to do from the previous section, I can say that we could probably get away with the following to reach that "MVP":
|
||||
|
||||
- User account creation. We'll keep only one type of user for now, but we do need to be aware that users will have different permission roles in the future
|
||||
|
||||
- Organizations creation/viewing (we can manually assign users to organizations using the database for now, but we'll want to structure data to support many users per organization)
|
||||
|
||||
- This org will need courses, so creation/view of those (no need to manage permissions, that'd be a future feature)
|
||||
|
||||
- Courses will need content, so a way to upload/view content on courses
|
||||
|
||||
While thinking about these features, I want to keep the implementation details to a minimum, just enough to suffice with our resources by ignoring the nuances of certain permission features. However, notice how, despite thinking about the features minimally, *I'm also mentally mapping how the data should be structured and thinking about long-term implications* in such a way that we can add them later without refactoring everything. This balance during architecture can be tough to achieve and becomes more and more natural with experience.
|
||||
|
||||
# Requirements {#data-requirements}
|
||||
|
||||
Finally, I look at the data requirements and features and start thinking about what code requirements I'll run into to implement those data requirements.
|
||||
|
||||
- I need to upload/download files
|
||||
|
||||
Speaking from experience, doing this with GraphQL is tricky, so I'll stick with REST for the MVP
|
||||
|
||||
- My data isn't likely to change structure very much
|
||||
|
||||
As a result, I'd feel comfortable using SQL for something like this.
|
||||
|
||||
- I need user authentication
|
||||
|
||||
I don't like rolling my own auth solution, so I'll probably use [passport](https://www.npmjs.com/package/passport) since it's been well tested and stable. If I want to enable users to sign in from their Google accounts or something in the future, I should keep that in mind even if I'm not building that functionality right away
|
||||
|
||||
- I am going to be focusing on per-user UI (achievements, dashboards, etc.)
|
||||
|
||||
As such, my use of something like [Gatsby](https://www.gatsbyjs.org/) for static site generation (SSG) isn't realistically beneficial. We could go with server-side rendering (SSR) with something like [Next.JS](https://nextjs.org/), but due to using a lot of media (video/picture), I'd argue there's not much of a return-on-investment (ROI) by building SSR-first since the content has to be loaded by the DOM regardless.
|
||||
|
||||
- I'm not likely to have many forms in my application - primarily focusing on viewing rather than form creation
|
||||
|
||||
Sometimes it's important to know what an application is and _isn't_ going to be using. If we were highly focused on forms, I might advocate for [Angular](https://angular.io/) to be used in the front-end (since I have found their form system to be quite robust). However, since I know my team is not as familiar with Angular as other options and we have a limited budget, we likely won't be moving forward with it
|
||||
|
||||
- However, we'll be hoping to have a lot of live-streamed user content in the future
|
||||
|
||||
Stuff like "live quizzes," live streaming/playback of video, anything that requires tracking of time/etc is all a great use case for event-based programming. One of the most prominent implementations of this in JavaScript is [RxJS](https://github.com/ReactiveX/rxjs).
|
||||
|
||||
So there we have it - a non-Angular, REST API, Passport authenticated, SQL DB, non-SSR, RxJS powered application
|
||||
|
||||
Now, this doesn't give us the whole idea, but from here we can start doing further research (next part)
|
||||
|
||||
# Extra Pieces
|
||||
|
||||
From here, things start becoming a lot more subjective and a lot more social.
|
||||
|
||||
While I personally prefer Vue, after talking with my team, it became clear that they're much more comfortable with React. Because React has a large ecosystem with a sturdy backing, I'm not against using it since I feel it can sustain our product's growth over time.
|
||||
|
||||
Moving onto CSS was more of the same: It was less "what can support this specific use-case" and more "what is familiar and can sustain our growth?".
|
||||
|
||||
This example is where things get really tricky because you often are not just picking a framework or library, but often a philosophy of CSS as well. After a long-form discussion with my (front-end focused) team about this, we decided to go with Styled Components and Material UI. These tools were decided on due to their flexibility, general A11Y support (for MUI), themability, and our comfort with the tools. The size and stability also took a role in this discussion.
|
||||
|
||||
Smaller decisions of libraries for me often boil down to a formula-of-sorts:
|
||||
|
||||
- What's their community support like? (can I ask a question and have an answer within a few days)
|
||||
- What's the size of their community? (typically judged by questions I can find on StackOverflow, their community site/forum or even npm downloads)
|
||||
- How stable is the tool?
|
||||
- When was it last updated?
|
||||
- Does it handle my edge-cases?
|
||||
- What's the performance of the tool?
|
||||
|
||||
Each tool and usage will weigh these questions differently. If I'm looking for a simple timer component library and come across two options, I may be more likely to pick a small library over a larger one, depending on the context. For example, _if that smaller library has clean, easily readable code, but only has seven stars on GitHub, that's likely better for my project than a more bloated alternative because I know I can maintain it if all else fails._ However, I personally wouldn't likely go with something like IO.js (now defunct alternative to Node.js) for a larger project regardless of how clean the code is because I'd be unable to maintain the much more complex tool if I ever needed to.
|
||||
|
||||
# Conclusion
|
||||
|
||||
To recap, it's a mixture of:
|
||||
- Proper planning (focusing on features and experience rather than tech)
|
||||
- This point should take double priority, as understanding what to start building after picking the tech is important
|
||||
- Expertise
|
||||
I knew that SQL would suffice for our data thanks to my experience scaffolding various applications with SQL and NoSQL alike
|
||||
- Research
|
||||
The only reason I knew that working with binary data over GQL is due to research I did ahead of time before even writing any product code
|
||||
- Communication
|
||||
This one is often overlooked but is **critical** - especially within teams. _Leverage each other's strengths and weaknesses and be open and receptive to suggestions/concerns_
|
||||
|
||||
That's by **no** means an easy feat to do, despite reading as if they were. Don't worry if you're not able to execute these skills flawlessly - goodness knows I can't! I'm sure a lot of the decisions I made here, even with the group I spoke to, could have been better guided in different ways. These are the skills that I think I value the most in seniors developer, especially communication. _Communication becomes critical when working with medium/larger teams (or really, groups of any size) since reasonable minds may differ on toolsets that they might see strengths/weaknesses in_.
|
||||
|
||||
Have a similar question to the one Lindsey asked? Like conversations like this? Have something to add? [Join us in our Discord server](https://discord.gg/FMcvc6T) to jump into the community and engage in conversations like this! We wouldn't have the quality of our content without our community!
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 63 KiB |
326
content/blog/integrating-android-code-in-unity/index.md
Normal file
@@ -0,0 +1,326 @@
|
||||
---
|
||||
{
|
||||
title: "Integrating Native Android Code in Unity",
|
||||
description: 'Have you ever wanted to run native Java and Kotlin code from your mobile game written in Unity? Well you can! This article outlines how to set that up!',
|
||||
published: '2020-01-04T05:12:03.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['unity', 'android', 'c#', 'java', 'kotlin'],
|
||||
attached: [],
|
||||
license: 'cc-by-nc-sa-4'
|
||||
}
|
||||
---
|
||||
|
||||
Working on mobile games with Unity, you may come across some instances where you'll want to run native code. Whether it be to access specific sensors, run code in the background, or other closer-to-hardware mobile-specific actions, knowing how to call native code from within your Unity's C# environment can be a great boon to your developmental efforts.
|
||||
|
||||
Luckily for us, Unity has a system of "plugins" that allow us to do just that. Unity contains the ability to map code between C# and Java by using in-house-developed helper classes to cross-talk between the two languages. This article will outline [how to set up a development environment](#set-up-a-development-environment), [how to manage Android dependencies in Unity](#android-dependencies), and finally [how to call Android-specific code from C#](#call-android-from-c-sharp). Without further ado, let's dive in! 🏊♂️
|
||||
|
||||
> ⚠️ Be aware that this information is based on Unity 2018 versions. While this might be relevant for older versions of Unity, I have not tested much of this methodology of integration with older versions.
|
||||
|
||||
# Setting up Development Environment {#set-up-a-development-environment}
|
||||
|
||||
[Unity supports using either Java files or Kotlin source files as plugins](https://docs.unity3d.com/Manual/AndroidJavaSourcePlugins.html). This means that you're able to take Android source files (regardless of if they're written in Java or Kotlin) and treat them as callable compiled library code. Unity will then take these files and then include them into its own Gradle build process, allowing you — the developer — to focus on development rather than the build process.
|
||||
|
||||
> For anyone who may have experimented with doing so in older versions of Unity in the past will note that this is a massive improvement — it used to be that you'd have to compile to AAR files and include them manually.
|
||||
|
||||
That said, the editor you may be using may not be best suited for editing Android code, and it would be great to have a powerful development experience while working with. For this purpose, it would be great to edit code using [the official IDE for Android development: Android Studio](https://developer.android.com/studio/).
|
||||
|
||||
Unfortunately, I've had difficulties getting the same Android Studio development environment to sync with the "source file" interoperability that Unity provides. For this reason, I tend to have two folders:
|
||||
|
||||
- One of these folders lives at the root of the project (directly under `Unity/ProjectName`) called `AndroidStudioDev` that I open in Android Studio.
|
||||
|
||||
- The other folder is one that lives under `Assets` called `AndroidCode`, which contains copied-and-pasted files from `AndroidStudioDev` that are only the related source files I need to call.
|
||||
|
||||

|
||||
|
||||
Once the copying of the files from the Android Studio environment to `Assets` has finished, you'll need to mark it as being included in the Android build within Unity's inspector window that comes up when you highlight the source file.
|
||||
|
||||

|
||||
|
||||
> If you forget to do this, your class or file may not be found. This is an important step to keep in mind during debugging.
|
||||
|
||||
This will naturally incur a question for developers who have tried to maintain a system of duplication of any size:
|
||||
**How do you manage dependencies between these two folders?**
|
||||
|
||||
|
||||
## Managing Android Dependencies {#android-dependencies}
|
||||
|
||||
Luckily for us, managing Android code dependencies in Unity has a thought-out solution from a large company: Google. [Because Google writes a Firebase SDK for Unity](https://firebase.google.com/docs/unity/setup), they needed a solid way to manage native dependencies within Unity.
|
||||
|
||||
### Installing the Unity Jar Resolver {#installing-jar-resolver}
|
||||
|
||||
> ℹ️ If you've installed the Unity Firebase SDK already, you may skip the step of installing.
|
||||
|
||||
[This plugin, called the "Unity Jar Resolver"](https://github.com/googlesamples/unity-jar-resolver/), is hugely useful to us for synchronizing our development environment. You can start by downloading it from [their releases tab on GitHub](https://github.com/googlesamples/unity-jar-resolver/releases).
|
||||
|
||||
> If you have a hard time finding the download link, you'll want to press the three dots (or, if you're looking for the alt text: the "Toggle commit message" button). There will typically be a link for downloading the `.unitypackage` file.
|
||||
|
||||
In your project, you'll then want to select `Assets > Import Package > Custom Package` in order to import the downloaded plugin.
|
||||
|
||||

|
||||
|
||||
Then, you'll see a dialog screen that'll ask what files you want to import with your Unity Package. Ensure that all of the files are selected, then press "Import".
|
||||
|
||||

|
||||
|
||||
> Your screen may look slightly different from the one above. That's okay — so long as all of the files are selected, pressing "Import" is perfectly fine.
|
||||
|
||||
### Using the Jar Resolver {#using-jar-resolver}
|
||||
|
||||
Using the Jar resolver is fairly straightforward. Whenever you want to use a dependency in your Android code, you can add them to a file within [the `Assets/AndroidCode` folder](#set-up-a-development-environment) that adds dependencies with the same keys as the ones typically found in a `build.gradle` file for dependencies.
|
||||
|
||||
```xml
|
||||
<!-- DeviceNameDependencies.xml -->
|
||||
<dependencies>
|
||||
<androidPackages>
|
||||
<androidPackage spec="com.jaredrummler:android-device-names:1.1.8">
|
||||
</androidPackage>
|
||||
</androidPackages>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
The only rule with this file structure is that your file must end with `Dependencies.xml`. You can have as many of these files as you'd like. Let's say you want to separate out dependencies based on features? You can do that, just have separate files that follow that naming pattern!
|
||||
|
||||
```xml
|
||||
<!-- LocationCodeDependencies.xml -->
|
||||
<!-- Alongside the other file -->
|
||||
<dependencies>
|
||||
<androidPackages>
|
||||
<androidPackage spec="com.google.android.gms:play-services-location:16.0.0">
|
||||
</androidPackage>
|
||||
</androidPackages>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
After creating the files, in the menubar, go to `Assets > Play Services Resolver > Android Resolver > Resolve`, and it should go fetch the AAR files related to those specific libraries and download them.
|
||||
|
||||

|
||||
|
||||
So long as your file ends with `Dependencies.xml`, it should be picked up by the plugin to resolve the AAR files.
|
||||
|
||||
#### Adding Support into Android Studio Environment {#add-android-studio-support}
|
||||
|
||||
But that's only half of the equation. When editing code in Android Studio, you won't be able to use the libraries you've downloaded in Unity. This means that you're stuck manually editing both of the locations for dependencies. This is where a simple trick with build files comes into play.
|
||||
|
||||
Assuming, like me, you used the built-in "Create Project" method of starting a codebase in Android Studio, you'll have a `build.gradle` file for managing dependencies. However, you'll notice that when you run the `Resolve` on the plugin in Unity, it'll download AAR and JAR files to `Assets/Plugins/Android`. You can tell Android Studio's Gradle to include them by adding the following line to your `dependencies`:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation fileTree(dir: '../../Assets/Plugins/Android', include: ['*.jar', '*.aar'])
|
||||
}
|
||||
```
|
||||
|
||||
This will take all of the AAR files and JAR files and treat them as if they were synced by Android Studio's Gradle sync.
|
||||
|
||||
|
||||
|
||||
For more information on how to manage your app's dependencies from within Unity, you may want to check out [this article created by the Firebase developers](https://medium.com/firebase-developers/how-to-manage-your-native-ios-and-android-dependencies-in-unity-like-firebase-921659843aef), who coincidentally made the plugin for managing Android dependencies in Unity.
|
||||
|
||||
|
||||
|
||||
# Call Android code from C# {#call-android-from-c-sharp}
|
||||
|
||||
It's great that we're able to manage those dependencies, but they don't mean much if you're not able to utilize the code from them!
|
||||
|
||||
For example, take the following library: https://github.com/jaredrummler/AndroidDeviceNames
|
||||
|
||||
That library allows you to grab metadata about a user's device. This might be useful for analytics or bug reporters you may be developing yourself. Let's see how we're able to integrate this Java library in our C# code when building for the Android platform.
|
||||
|
||||
## Introduction {#intro-call-android-from-c-sharp}
|
||||
|
||||
You must make your callback extend the type of callback that is used in the library. For example, take the following code sample from the README of the library mentioned above:
|
||||
|
||||
```java
|
||||
DeviceName.with(context).request(new DeviceName.Callback() {
|
||||
@Override public void onFinished(DeviceName.DeviceInfo info, Exception error) {
|
||||
String manufacturer = info.manufacturer; // "Samsung"
|
||||
String name = info.marketName; // "Galaxy S8+"
|
||||
String model = info.model; // "SM-G955W"
|
||||
String codename = info.codename; // "dream2qltecan"
|
||||
String deviceName = info.getName(); // "Galaxy S8+"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
While this example may seem straightforward, let's dissct what we're doing step-by-step here. This will allow us to make the migration to C# code much simpler to do mentally.
|
||||
|
||||
```java
|
||||
// Create a new "DeviceName.Callback" instance
|
||||
DeviceName.Callback handleOnFinished = new DeviceName.Callback() {
|
||||
// Provide an implementation of the `onFinished` function in the `Callback` class
|
||||
// Notice that there are two parameters for this method: one for info, the other for errors
|
||||
@Override public void onFinished(DeviceName.DeviceInfo info, Exception error) {
|
||||
// ... Assignment logic here
|
||||
}
|
||||
};
|
||||
|
||||
// Create a `DeviceName.Request` by passing the current context into the `DeviceName.with` method
|
||||
DeviceName.Request withInstance = DeviceName.with(context);
|
||||
|
||||
// Use that request instance to pass the `DeviceName.Callback` instance from above to run the related code
|
||||
withInstance.request(handleOnFinished);
|
||||
```
|
||||
|
||||
You can see that we have a few steps here:
|
||||
|
||||
1) Make a new `Callback` instance
|
||||
- Provide an implementation of `onFinished` for said instance
|
||||
2) Call `DeviceName.with` to create a request we can use later
|
||||
- This means that we have to gain access to the currently running context to gain device access. When calling the code from Unity, it means we have to get access to the `UnityPlayer` context that Unity engine runs on
|
||||
3) Call that request's `request` method with the `Callback` instance
|
||||
|
||||
For each of these steps, we need to have a mapping from the Java code to C# code. Let's walk through these steps one-by-one
|
||||
|
||||
## Create `Callback` Instance {#android-c-sharp-callback}
|
||||
|
||||
In order to create an instance of a `Callback` in C# code, we first need a C# class that maps to the `Java` interface. To do so, let's start by extending the Android library interface. We can do this by using the `base` constructor of `AndroidJavaProxy` and the name of the Java package path. You're able to use `$` to refer to the interface name from within the Java package.
|
||||
|
||||
```csharp
|
||||
private class DeviceCallback : AndroidJavaProxy
|
||||
{
|
||||
// `base` calls the constructor on `AndroidJava` to pass the path of the interface
|
||||
// `$` refers to interface name
|
||||
public DeviceCallback() : base("com.jaredrummler.android.device.DeviceName$Callback") {}
|
||||
}
|
||||
```
|
||||
|
||||
> [This package path can be found in the library's code at the following path](https://github.com/jaredrummler/AndroidDeviceNames/blob/e23b73dbb81be6cb64dfa541a3e93800ee26b185/library/src/main/java/com/jaredrummler/android/device/DeviceName.java#L17). The `DeviceName` is referring to the path of the `.java` file name.
|
||||
|
||||
We can then provide an implementation of the `onFinished` method of that `Callback`. Recall how we previously had two params? Well, now the implementation will require we use the `AndroidJavaObject` type for both of those params.
|
||||
|
||||
Otherwise — if we type the function with a C# interface or class that matches the Java implementation — the method will not be called when we expect it to. This is due to function overloading expecting to get the `AndroidJavaObject` from the code Unity has developed to call mapped functions and classes.
|
||||
|
||||
This [`AndroidJavaObject` type has a myriad of methods that can be called to assist in gathering data from or interfacing with the Java object](https://docs.unity3d.com/ScriptReference/AndroidJavaObject.html). One of such methods is the [`Get` method](https://docs.unity3d.com/ScriptReference/AndroidJavaObject.Get.html). When called on an `AndroidJavaObject` instance in C#, it allows you to grab a value from Java. Likewise, if you intend to call a method from the Java code, you can use [`AndroidJavaObject.Call`](https://docs.unity3d.com/ScriptReference/AndroidJavaObject.Call.html).
|
||||
|
||||
```csharp
|
||||
private class DeviceCallback : AndroidJavaProxy
|
||||
{
|
||||
public DeviceCallback() : base("com.jaredrummler.android.device.DeviceName$Callback") {}
|
||||
// These both MUST be `AndroidJavaObject`s. If not, it won't match the Java method type and therefore won't be called
|
||||
void onFinished(AndroidJavaObject info, AndroidJavaObject err)
|
||||
{
|
||||
// When running `AndroidJavaObject` methods, you need to provide a type for the value to be assigned to
|
||||
var manufacturer = info.Get<string>("manufacturer"); // "Samsung"
|
||||
var readableName = info.Get<string>("marketName"); // "Galaxy S8+"
|
||||
var model = info.Get<string>("model"); // "SM-G955W"
|
||||
var codename = info.Get<string>("codename"); // "dream2qltecan"
|
||||
var deviceName = info.Call<string>("getName"); // "Galaxy S8+"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Get Current Context {#get-unity-context}
|
||||
|
||||
Just as all Android applications have some context to their running code, so too does the compiled Unity APK. When compiling down to Android, Unity includes a package called the "UnityPlayer" to run the compiled Unity code. The package path for the player in question is `com.unity3d.player.UnityPlayer`.
|
||||
|
||||
While there is not a docs reference page for this Java class, [some of the company's code samples](https://docs.unity3d.com/530/Documentation/Manual/PluginsForAndroid.html) provide us with some useful methods and properties on the class. For example, that page mentions a static property of `currentActivity` that gives us the context we need to pass to `DeviceName.with` later on:
|
||||
|
||||
```csharp
|
||||
var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
|
||||
var activity = player.GetStatic<AndroidJavaObject>("currentActivity");
|
||||
```
|
||||
|
||||
We can then gain access to the `DeviceName` Java class. If we look at [the related Java code from the previous section](#call-android-from-c-sharp), we can see that we're calling `DeviceName.with` without making a new instance of `DeviceName`:
|
||||
|
||||
```java
|
||||
DeviceName.Request withInstance = DeviceName.with(context);
|
||||
```
|
||||
|
||||
This means that `with` must be a static method on the `DeviceName` class. In order to call static Java methods, we'll use the `AndroidJavaClass.CallStatic` method in C#.
|
||||
|
||||
```csharp
|
||||
var jc = new AndroidJavaClass("com.jaredrummler.android.device.DeviceName");
|
||||
var withCallback = jc.CallStatic<AndroidJavaObject>("with", activity);
|
||||
```
|
||||
|
||||
Finally, we can add the call to `request` with an instance of the `DeviceCallback` class.
|
||||
|
||||
```csharp
|
||||
var deviceCallback = new DeviceCallback();
|
||||
withCallback.Call("request", deviceCallback);
|
||||
```
|
||||
|
||||
## Complete Code Example {#android-c-sharp-code-sample}
|
||||
|
||||
Line-by-line explanations are great, but often miss the wholistic image of what we're trying to achieve. The following is a more complete code sample that can be used to get device information from an Android device from Unity.
|
||||
|
||||
```csharp
|
||||
public class DeviceInfo {
|
||||
public string manufacturer; // "Samsung"
|
||||
public string readableName; // "Galaxy S8+"
|
||||
public string model; // "SM-G955W"
|
||||
public string codename; // "dream2qltecan"
|
||||
public string deviceName; // "Galaxy S8+"
|
||||
}
|
||||
|
||||
class DeviceName : MonoBehaviour {
|
||||
private class DeviceCallback : AndroidJavaProxy {
|
||||
// Add in a field for us to gain access to the device info after the callback has ran
|
||||
public DeviceInfo deviceInfo;
|
||||
public DeviceCallback() : base("com.jaredrummler.android.device.DeviceName$Callback") {}
|
||||
void onFinished(AndroidJavaObject info, AndroidJavaObject err) {
|
||||
deviceInfo.manufacturer = info.Get<string>("manufacturer");
|
||||
deviceInfo.readableName = info.Get<string>("marketName");
|
||||
deviceInfo.model = info.Get<string>("model");
|
||||
deviceInfo.codename = info.Get<string>("codename");
|
||||
deviceInfo.deviceName = info.Call<string>("getName");
|
||||
}
|
||||
}
|
||||
|
||||
private void Start() {
|
||||
var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
|
||||
var activity = player.GetStatic<AndroidJavaObject>("currentActivity");
|
||||
var jc = new AndroidJavaClass("com.jaredrummler.android.device.DeviceName");
|
||||
var withCallback = jc.CallStatic<AndroidJavaObject>("with", activity);
|
||||
var deviceCallback = new DeviceCallback();
|
||||
withCallback.Call("request", deviceCallback);
|
||||
Debug.Log(deviceCallback.deviceInfo.deviceName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Calling Source Code from Unity {#call-source-from-unity}
|
||||
|
||||
Calling native Android code can be cool, but what if you have existing Android code you want to call from Unity? Well, that's supported as well. Let's take the following Kotlin file:
|
||||
|
||||
```kotlin
|
||||
// Test.kt
|
||||
package com.company.example
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
|
||||
class Test() {
|
||||
fun runDebugLog() {
|
||||
Log.i("com.company.example", "Removing location updates")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Assuming you [copied it over to the `Assets/AndroidCode` folder and marked it to be included in the Android build](#set-up-a-development-environment), you should be able to use the `package` name and the name of the class in order to run the related code.
|
||||
|
||||
```csharp
|
||||
var testAndroidObj = new AndroidJavaObject("com.company.example.Test");
|
||||
testAndroidObj.Call("runDebugLog");
|
||||
```
|
||||
|
||||
# AndroidManifest.XML Overwriting {#manifest-file}
|
||||
|
||||
Many Android app developers know how important it can be to have the ability to customize their manifest file. By doing so, you're able to assign various metadata to your application that you otherwise would be unable to. Luckily for us, Unity provides the ability to overwrite the default XML file.
|
||||
|
||||
By placing a file under `Assets\Plugins\Android\AndroidManifest.xml`, you're able to add new values, change old ones, and much more.
|
||||
|
||||
If you want to find what the default manifest file looks like, you'll want to look for the following file: `<UnityInstallationDirecory>\Editor\Data\PlaybackEngines\AndroidPlayer\Apk\AndroidManifest.xml`. This file is a good baseline to copy into your project to then extend upon. The reason I suggest starting with the default XML is that Unity requires its own set of permissions and such. After that, however, you're able to take the manifest and customize it to your heart's content.
|
||||
|
||||
> It's worth mentioning that if you use Firebase Unity SDK and wish to provide your own manifest file, you'll need to [customize the default manifest file to support Firebase opperations](https://firebase.google.com/docs/cloud-messaging/unity/client#configuring_an_android_entry_point_activity).
|
||||
|
||||
# Firebase Support {#firebase}
|
||||
|
||||
Let's say you're one of the users who utilizes the Firebase SDK for Unity. What happens if you want to send data from Android native code or even use background notification listeners in your mobile app?
|
||||
|
||||
You're in luck! Thanks to the Unity Firebase plugin using native code in the background, you're able to share your configuration of Firebase between your native and Unity code. So long as you've [configured Firebase for Unity properly](https://firebase.google.com/docs/cloud-messaging/unity/client#add-config-file) and [added the config change to Android Studio](#add-android-studio-support), you should be able to simply call Firebase code from within your source files and have the project configs carry over. This means that you don't have to go through the tedium of setting up and synchronizing the Unity and Android config files to setup Firebase — simply call Firebase code from your source files, and you should be good-to-go! No dependency fiddling required!
|
||||
|
||||
# Conclusion {#conclusion}
|
||||
|
||||
I hope this article has been helpful to anyone hoping to use Android code in their Unity mobile game; I know how frustrating it can be sometimes to get multiple moving parts to mesh together to work. Rest assured, once it does, it's a satisfying result knowing that you're utilizing the tools that Unity and the Firebase team have so graciously provided to game developers.
|
||||
|
||||
If you have any questions or comments, please leave them down below. Thanks for reading!
|
||||
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -4,7 +4,7 @@
|
||||
description: 'Introduction to the underlying concepts of HTML, CSS, and JavaScript and how they work together.',
|
||||
published: '2019-12-16T13:45:00.284Z',
|
||||
authors: ['MDutro'],
|
||||
tags: ['HTML', 'CSS', 'JavaScript', 'Beginner'],
|
||||
tags: ['html', 'css', 'javascript'],
|
||||
attached: [],
|
||||
license: 'publicdomain-zero-1'
|
||||
}
|
||||
@@ -57,7 +57,7 @@ Just like CSS, JavaScript is normally written in a separate file and connected b
|
||||
|
||||
As I said before, JavaScript is a for-real programming language. That means it has arrays, for loops, if-else statements, and lots of other computer science-y things going on. Despite that, the language is actually very beginner-friendly. You don’t have to have a degree in computer science or arcane mathematics to get started with a programming language. And because of the way JavaScript interacts with web browsers, you will be to do some amazing things pretty quickly.
|
||||
|
||||
One thing that makes JavaScript unique is [its ability to manipulate the DOM](https://unicorn-utterances.com/posts/understanding-the-dom/). The Document Object Model (or DOM) is an API (advanced programming interface) that allows JavaScript to manipulate the HTML and CSS of a website as the user navigates around the page and uses its features. Basically, the web browser can read JavaScript and make changes to the look, feel, and even the structure of the page in real-time.
|
||||
One thing that makes JavaScript unique is [its ability to manipulate the DOM](/posts/understanding-the-dom/). The Document Object Model (or DOM) is an API (advanced programming interface) that allows JavaScript to manipulate the HTML and CSS of a website as the user navigates around the page and uses its features. Basically, the web browser can read JavaScript and make changes to the look, feel, and even the structure of the page in real-time.
|
||||
|
||||
To go back to our building construction analogy… well, it starts to break down at this point. Imagine if you could wave a magic wand and turn the wood siding on your building into bricks. Or change the color of the building from gray to bright blue. Remember the steel beams of HTML our building is made of on the inside? By using the DOM, JavaScript can change those too!
|
||||
|
||||
@@ -68,6 +68,6 @@ JavaScript is a powerful tool that can be used to create everything from useful
|
||||
|
||||
Now you should have a better conceptual understanding of the primary web technologies, what they do, and how they work together to create the internet that we see and use every day. Once you learn the basics of HTML, CSS, and JavaScript, you will have a firm foundation to build on to create your own websites and applications.
|
||||
|
||||
You can also read more about how your browser understands and utilizes HTML and CSS in order to display content and handle user interaction under-the-hood on [another post on the site](https://unicorn-utterances.com/posts/understanding-the-dom/).
|
||||
You can also read more about how your browser understands and utilizes HTML and CSS in order to display content and handle user interaction under-the-hood on [another post on the site](/posts/understanding-the-dom/).
|
||||
|
||||
Finally, you're always able to [join our Discord](https://discord.gg/FMcvc6T) if you have any questions or comments while you're learning. All are welcome!
|
||||
|
||||
@@ -214,6 +214,6 @@ Essentially, I just want to make sure to iterate that while there may be tools t
|
||||
|
||||
And with that, we have a better understanding of what TypeScript is! I hope this has been informative and helpful for those that may be new to the language in particular. What'd you learn, let us know!
|
||||
|
||||
Now that you're more familiar with TypeScript, maybe you'd like to play around with one of their more experienced functionality: [Type generics](https://unicorn-utterances.com/posts/typescript-type-generics/)? We have a whole post around that concept as well, [you can find that here](https://unicorn-utterances.com/posts/typescript-type-generics/).
|
||||
Now that you're more familiar with TypeScript, maybe you'd like to play around with one of their more experienced functionality: [Type generics](/posts/typescript-type-generics/)? We have a whole post around that concept as well, [you can find that here](https://unicorn-utterances.com/posts/typescript-type-generics/).
|
||||
|
||||
Thanks for reading! Leave any questions or feedback in the comments below.
|
||||
|
||||
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 272 KiB |
614
content/blog/making-a-slack-bot-with-node-and-mongo/index.md
Normal file
@@ -0,0 +1,614 @@
|
||||
---
|
||||
{
|
||||
title: "Making a Slack Bot using NodeJS and MongoDB",
|
||||
description: 'Join us as we teach you how to create a Slack bot from scratch using their Node SDK and MongoDB for persistence',
|
||||
published: '2020-02-18T05:12:03.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['mongodb', 'node', 'slack'],
|
||||
attached: [],
|
||||
license: 'cc-by-nc-sa-4'
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
Modern-day remote live communication has never been as efficient or fast as it is today. Services like Slack make it easy to join huge multi-channel communication workspaces for pleasure or business. These channels are often able to be super-powered by in-chat bots and applications that can inform you of new information from external services or even add new functionality to the chat. Luckily for us, Slack has put a lot of effort into making these extensions to Slack easy to write.
|
||||
|
||||
One way they've made extension development easier is by providing an SDK for Node developers to use and create extensions with. This post will outline how we can create a Slack bot to add functionality to chats.
|
||||
|
||||
# Initial Signup {#signup-for-dev-account}
|
||||
|
||||
To start, we'll need to [sign up for a developer account and create an app to host our application logic using this link](https://api.slack.com/apps). This will allow us to create new Slack apps and bots to add into our workspace.
|
||||
|
||||

|
||||
|
||||
Enter in an app name, and assign the workspace where you want the app to live during development. Once done, you should be greeted by a dashboard for your Slack app. You'll want to keep this screen open during development, as we'll be referring to it throughout this post.
|
||||
|
||||

|
||||
|
||||
This screen (and the tabs off to the side) provides the configuration for all of the interactions with Slack that we'll build upon with our code. We're even able to customize the look of our application in our Slack settings at the bottom of this homepage.
|
||||
|
||||

|
||||
|
||||
As mentioned previously, Slack provides an SDK for Node applications. [You can find the homepage for the npm package at the following URL.](https://github.com/slackapi/node-slack-sdk)
|
||||
|
||||
In order to quickly set up the SDK, we'll create a new directory for our code to live. Once we have a clear directory, we can run:
|
||||
|
||||
```
|
||||
npm init -y
|
||||
```
|
||||
|
||||
To setup an initial `package.json`. Once we have a `package.json`, we can add the packages we require to use the Slack SDK:
|
||||
|
||||
```
|
||||
npm install @slack/web-api @slack/events-api
|
||||
```
|
||||
|
||||
After this, we'll then be able to use their example API from the README of their project as a starter for our app:
|
||||
|
||||
```javascript
|
||||
// index.js
|
||||
// Initialize using signing secret from environment variables
|
||||
const { createEventAdapter } = require('@slack/events-api');
|
||||
// Slack requires a secret key to run your bot code. We'll find and figure out this signing secret thing in the next steps
|
||||
const slackEvents = createEventAdapter(process.env.SLACK_SIGNING_SECRET);
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// Attach listeners to events by Slack Event "type". See: https://api.slack.com/events/message.im
|
||||
slackEvents.on('message', (event) => {
|
||||
console.log(`Received a message event: user ${event.user} in channel ${event.channel} says ${event.text}`);
|
||||
});
|
||||
|
||||
// Handle errors (see `errorCodes` export)
|
||||
slackEvents.on('error', console.error);
|
||||
|
||||
// Start a basic HTTP server
|
||||
slackEvents.start(port).then(() => {
|
||||
// Listening on path '/slack/events' by default
|
||||
console.log(`server listening on port ${port}`);
|
||||
});
|
||||
```
|
||||
|
||||
This code is what we'll need to run a `console.log` every time a user sends a message. However, _we'll need to set things up more to get this code actually working due to Slack's permissions systems_ and such. For now, we'll save this code to `index.js` in the same folder we saved our `package.json` file.
|
||||
|
||||
Another thing that was mentioned in the code sample was the `process.env.SLACK_SIGNING_SECRET`. This is the key that Slack will use to connect your code to your workspace. We'll want to keep in mind how to store the signing secret (as the name implies, _we want to keep this key a secret as otherwise anyone can hijack your Slack app_). As the above code hints at, it's suggested to use an environment variable file or configuration.
|
||||
|
||||
While environment variables are typically assigned by system configurations, we'll make development easier by setting up a `.env` file with the expected credentials. Then, to inject the `.env` file contents into our process, we'll run our code using [the `env-cmd` package](https://www.npmjs.com/package/env-cmd). We'll start by installing the package:
|
||||
|
||||
```
|
||||
npm i env-cmd
|
||||
```
|
||||
|
||||
This package will look for a `.env` file and inject it into your command that follows `env-cmd`. So, for example, you can **make a new file called `.env` and place the following contents in it**:
|
||||
|
||||
```
|
||||
SLACK_SIGNING_SECRET=<SIGNING_SECRET_FROM_HOMESCREEN>
|
||||
```
|
||||
|
||||
Then, in your `package.json`, you can **edit your `start` script** to reflect the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"start": "env-cmd node ./index.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, whenever your code uses `process.env.SLACK_SIGNING_SECRET`, it'll represent the value present in your `.env` file.
|
||||
|
||||
# Development Hosting {#development-environment-setup}
|
||||
|
||||
In order to have these events called, we'll need to get a public URL to route to our local development server. In order to do this, we can [use `ngrok`](https://github.com/inconshreveable/ngrok) to host a public URL in our local environment:
|
||||
|
||||
```
|
||||
npm i -D ngrok
|
||||
npx ngrok http 3000
|
||||
```
|
||||
|
||||
> Keep in mind that this should NOT be used to host your Slack application when you're ready to publish.
|
||||
> This should only be used during development process. In order to see how to deploy, you'll want to check out [the section on doing so using Heroku](#deployment).
|
||||
|
||||
After doing so, you should be given an `ngrok.io` subdomain to map to your local IP address with a message like this:
|
||||
|
||||
```
|
||||
Forwarding https://9fca9f3e.ngrok.io -> http://localhost:3000
|
||||
```
|
||||
|
||||

|
||||
|
||||
We're now able to use this URL as a bridge between the external internet and the local environment we're in. This is how we'll tell Slack to run our `index.js` file when we receive a new event.
|
||||
|
||||
However, there's yet another step to enable the functionality. Slack, in order to ensure security, wants to confirm that you own this domain. As such, they have _a utility you'll need to run to ensure that you own the domain_. So, for example, in order to add in the events subscription to our current code, we'll run the following command:
|
||||
|
||||
```
|
||||
./node_modules/.bin/slack-verify --secret <signing_secret>
|
||||
```
|
||||
|
||||
Where the `<signing_secret>` is the same signing secret from the `.env` file.
|
||||
|
||||

|
||||
|
||||
With this command still running, you can **press on the "Add features and functionality" tab** in the homescreen you saw when you first created your Slack app in the browser. Once the "features and functionality" is open, **press "Event Subscriptions"**.
|
||||
|
||||
This will bring you to a page with an "On/Off" toggle. **Toggle it to "On"** and **add the `ngrok` domain** in the request URL.
|
||||
|
||||

|
||||
|
||||
This should show "Verified" to explain that your domain is verified to have belonged to you, but the domain isn't saved yet; We first need to **add workspace events to subscribe to**. This is to ensure that any app doesn't simply have root permissions to everything for privacy and security's sake and instead has to ask for granular permissions.
|
||||
|
||||

|
||||
|
||||
Let's say we want to handle all of the public messages to a channel, we can add `message.channels` to get the permissions to do so.
|
||||
|
||||

|
||||
|
||||
If you look through the code that we now have in the `index.js` file, you'll see that we're listening for `messages`:
|
||||
|
||||
```javascript
|
||||
slackEvents.on('message', (event) => {
|
||||
console.log(`Received a message event: user ${event.user} in channel ${event.channel} says ${event.text}`);
|
||||
});
|
||||
```
|
||||
|
||||
I can hear you asking "But here we're requesting `message.channels`, how do we know that those two match each other?"
|
||||
|
||||
You can actually check the event `type` from [the API reference documentation](https://api.slack.com/events/message.channels) to see that the `type`s match up.
|
||||
|
||||
# Development App Installation {#development-installation}
|
||||
|
||||
You'll notice, as I first did, that if you start your server with `npm start` and then send a message to a public channel that you'll notice something in your terminal. Or, well, rather, a lack of something in your terminal. The `console.log` that you would expect to run isn't doing so - why is that?
|
||||
|
||||
That's because the app isn't actually enabled in your workspace yet (A real 🤦♂️ for me when I discovered this one).
|
||||
|
||||
To do so, check the sidebar to the right of your Slack API homepage for the `install` section:
|
||||
|
||||

|
||||
|
||||
Simply click `Install App to Workspace`, then `Allow` to give permissions to add the app to your workspace.
|
||||
|
||||
> Keep in mind, folks can use Slack for personal communication. You may want to give folks in your workspace a heads-up or simply create a new Slack workspace for testing.
|
||||
|
||||
Once this is done, you can send a test message to a public channel and see it printed out in your console!
|
||||
|
||||

|
||||
|
||||
# App Interactivity {#interactive-message-package}
|
||||
|
||||
While listening to events alone can be very useful in some circumstances, oftentimes having a way to interact with your application can be very helpful. As a result, the Slack SDK also includes the `@slack/interactive-messages` package to help you provide interactions with the user more directly. Using this package, you can respond to the user's input. For example, let's say we wanted to replicate the [PlusPlus](https://go.pluspl.us/) Slack bot as a way to track a user's score.
|
||||
|
||||
We want to have the following functionality for an MVP:
|
||||
|
||||
- `@UserOrThing++`: A way to add a point to a user or thing
|
||||
- `@UserOrThing--`: A way to remove a point from a user or thing
|
||||
- `@PointsRUs leaderboard`: A flat list of the items/people with points
|
||||
|
||||
Each of these messages will prompt the bot to respond with a message in the same channel. Ideally we'd use a database to store score for long-term projects, but for now, let's use in-memory storage for an MVP of the interactivity we're hoping for.
|
||||
|
||||
## Setup {#interactive-bot-setup}
|
||||
|
||||
First and foremost, something you'll need to do is add a new OAuth permission to enable the functionality for the bot to write to the channel. Go into the dashboard and go to the "OAuth & Permissions" tab. The second section of the screen should be called "Scopes", where you can add the `chat:write:bot` permission.
|
||||

|
||||
|
||||
After enabling the new OAuth permission, you'll need to reinstall your app. This is because you're changing the permissions of your apps and you need to accept the new permissions when you reinstall the app. If you scroll to the top of the same OAuth page, you should see a `Reinstall App` button that will help you do this easily.
|
||||
|
||||

|
||||
|
||||
Once this is done, you can access the OAuth token for the fresh installation of your workspace. This token will enable us to send messages to the workspace itself. It acts as a user-login of sorts for your Slack bot.
|
||||
|
||||
> This token is unique per-workspace, so if you're intending for a broader release of your bot (to be easily added to multiple workspaces with a single button click), you'll likely need to [walk through their OAuth token request system](https://api.slack.com/authentication/oauth-v2#asking). Since this is meant as an introductory look at their APIs, we'll simply keep things locally and copy-paste.
|
||||
|
||||
Copying the token from the top of the screen, store it into our `.ENV` file so that we can utilize it in our application. I named the environment variable `OAUTH_TOKEN`, so when you see that in code examples, know that this is in reference to this value.
|
||||
|
||||
## The Code {#leaderboard-local-code}
|
||||
|
||||
To start adding in response functionality, we need to install the package that'll allow us to use the web API:
|
||||
|
||||
```
|
||||
npm i @slack/web-api
|
||||
```
|
||||
|
||||
The web API should enable us to use the [`postMessage`](https://api.slack.com/methods/chat.postMessage) method to send messages to a channel when they send a message.
|
||||
|
||||
Once this is installed, we're able to instantiate the web API with the OAuth token we grabbed earlier
|
||||
|
||||
```javascript
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
const token = process.env.OAUTH_TOKEN;
|
||||
const web = new WebClient(token);
|
||||
```
|
||||
|
||||
After this is setup, we could run code like:
|
||||
|
||||
```javascript
|
||||
web.chat.postMessage({
|
||||
text: 'A post message',
|
||||
channel: channelId,
|
||||
});
|
||||
```
|
||||
|
||||
To send a message. Let's try to use this API to add some trivial logic into our existing `events` listening functionality.
|
||||
|
||||
```javascript
|
||||
const { createEventAdapter } = require('@slack/events-api');
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
|
||||
const token = process.env.OAUTH_TOKEN;
|
||||
const slackSigningSecret = process.env.SLACK_SIGNING_SECRET;
|
||||
|
||||
const slackEvents = createEventAdapter(slackSigningSecret);
|
||||
const web = new WebClient(token);
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
slackEvents.on('message', async event => {
|
||||
console.log(`Received a message event: user ${event.user} in channel ${event.channel} says ${event.text}`);
|
||||
|
||||
// Check if the text includes the text we'd want to use to check the leaderboard
|
||||
if (/@pointsrus leaderboard/i.exec(event.text)) {
|
||||
const result = await web.chat.postMessage({
|
||||
// We'll add more functionality in the future. We just want to test it works, first
|
||||
text: 'This should output a leaderboard',
|
||||
channel: event.channel,
|
||||
});
|
||||
|
||||
console.log(`Successfully send message ${result.ts} in conversation ${event.channel}`);
|
||||
}
|
||||
});
|
||||
|
||||
slackEvents.on('error', console.error);
|
||||
|
||||
slackEvents.start(port).then(() => {
|
||||
console.log(`server listening on port ${port}`);
|
||||
});
|
||||
```
|
||||
|
||||
As it did before, the code will listen for every message we send. Then, we listen for any time the user typed the message `@pointsrus leaderboard` and respond with a placeholder value when they do so. We're making sure to use the same channel ID by using the `event.channel` property.
|
||||
|
||||
> Remember, the channel ID is not the same thing as the human-readable channel name. It's a unique ID generated by Slack and as such you'd have to use the API to get the channel ID if you only knew the human-readable name
|
||||
|
||||
## Adding State {#interactive-local-state}
|
||||
|
||||
Luckily for our MVP, we've already outlined that we won't be using a database for the initial version of the bot. As such, we're able to keep a simple stateful object and simply mutate it to keep track of what's being scored.
|
||||
|
||||
For example, given a mutable `state` variable, we can do actions to read and write as such:
|
||||
|
||||
```javascript
|
||||
const state = {};
|
||||
state.word1 = 1;
|
||||
state.word1 = state.word1 + 1;
|
||||
state.word2 = -1;
|
||||
console.log(state); // {word1: 2, word2: -1}
|
||||
```
|
||||
|
||||
Following this pattern, let's go through and add a few lines of code to the last example to fulfill the expected behavior:
|
||||
|
||||
```javascript
|
||||
const { tablize } = require('batteries-not-included/utils');
|
||||
|
||||
/**
|
||||
* @type <Record<string, number>> A record of the word and score. Should start at 0.
|
||||
* This should be replaced by a database for persistence. This is just a demo and as
|
||||
* such simply mutates this object to be stateful.
|
||||
*/
|
||||
const state = {};
|
||||
|
||||
/**
|
||||
* A function that accepts a string, then returns the action and the word to score.
|
||||
*/
|
||||
const getIsPlusOrMinus = str => {
|
||||
// Accept em-dash for cases like MacOS turning -- into an emdash
|
||||
const plusOrMinusRegex = /\@(\w+?)(\-{2}|\+{2}|\—{1})/;
|
||||
// The first item in the array is the full string, then the word to score, then the opperator
|
||||
const [_, itemToScore, scoreStr] = plusOrMinusRegex.exec(str) || [];
|
||||
switch (scoreStr) {
|
||||
case '--':
|
||||
case '—':
|
||||
return { action: 'minus', word: itemToScore };
|
||||
case '++':
|
||||
return { action: 'add', word: itemToScore };
|
||||
default:
|
||||
return { action: '', word: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
slackEvents.on('message', async event => {
|
||||
console.log(`Received a message event: user ${event.user} in channel ${event.channel} says ${event.text}`);
|
||||
|
||||
const { action, word } = getIsPlusOrMinus(event.text);
|
||||
// If the `event.text` did not include a score (of plus or minus), it will return `{}`
|
||||
// And therefore `action` will be `undefined`
|
||||
if (action) {
|
||||
const currentState = state[word] || 0;
|
||||
// Mutate the state to update the score of the word.
|
||||
state[word] = action == 'add' ? currentState + 1 : currentState - 1;
|
||||
const actionString = action == 'add' ? 'had a point added' : 'had a point removed';
|
||||
const result = await web.chat.postMessage({
|
||||
text: `${word} ${actionString}. Score is now at: ${state[word]}`,
|
||||
channel: event.channel,
|
||||
});
|
||||
|
||||
console.log(`Successfully send message ${result.ts} in conversation ${event.channel}`);
|
||||
}
|
||||
|
||||
if (/@pointsrus leaderboard/i.exec(event.text)) {
|
||||
// Tablize just takes a 2D array, treats the first item as a header row, then makes an ASCII table
|
||||
const tableString = tablize([['Item', 'Count'], ...Object.entries(state)]);
|
||||
|
||||
// Send the table in a code block to use a monospace font and render properly.
|
||||
const result = await web.chat.postMessage({
|
||||
text: '```\n' + tableString + '```',
|
||||
channel: event.channel,
|
||||
});
|
||||
|
||||
console.log(`Successfully send message ${result.ts} in conversation ${event.channel}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
As you can see, we're able to add in the functionality for the score-keeping relatively easily with little additional code. Slightly cheating, but to pretty-print the score table, we're using a `tablize` package that's part of [the "batteries not included" library we've built](https://github.com/unicorn-utterances/batteries-not-included) in order to provide an ASCII table for our output.
|
||||
|
||||
# Adding a Database {#mongodb}
|
||||
|
||||
Even though the bot works well so far, it's not ideal to keep a score in memory. If your server crashes or if there's any other form of interruption in the process running, you'll lose all of your data. As such, we'll be replacing our local store with a database. As our data needs are simple and I want to keep this article relatively short, let's use a NoSQL database to avoid having to structure tables. We'll use MongoDB in order to keep our data stored.
|
||||
|
||||
> This section will cover the setup of MongoDB Atlas, if you'd like to [skip ahead to the code section where we switch our in-memory store with a MongoDB database, you can click here](#mongodb-code)
|
||||
|
||||
To remain consistent in keeping our app setup as trivial as possible, we'll be using MongoDB Atlas. Atlas enables us to have a serverless MongoDB service at our disposal. In order to use Atlas, you'll need to [sign up for an account](https://cloud.mongodb.com/user#/atlas/register/accountProfile).
|
||||
|
||||
Once done, you'll need to "Build a new cluster" in order to create a database cluster for your Slack app.
|
||||
|
||||

|
||||
|
||||
From here, you'll select the cloud provider that you'll use to host your database. There's AWS, Google Cloud Platform, and Azure. All three of these options have a Free tier that you can use to host smaller applications and have **plenty** of storage and run time for smaller projects.
|
||||
|
||||

|
||||
|
||||
> While all three have free tiers, you're limited to one free cluster per account. I have already created one, which is why it shows the price in the screenshot above. Yours should be free if you select one of the "Free tier available" hosting locations and read the instructions.
|
||||
|
||||
Once the cluster is created, it may take some time to propagate the changes to the hosting solution itself. Once it is done, however, we're able to create a new user for database access. This will allow you to create a user for your MongoDB code to connect to a make interactions. Go to the "Database Access" tab of Atlas and press "Add New User",
|
||||
|
||||

|
||||
|
||||
Once there, you'll add a username and password. You'll also want to enable the permission to read and write to a database, seeing as we'll be editing the scores collection in the database.
|
||||
|
||||

|
||||
|
||||
> Be sure to remember that password! You'll want to store it in your .ENV file as plain text (so be sure you're on a secured computer! You do not want to store your passwords in such insecure ways for production).
|
||||
|
||||
We'll store the MongoDB username and password into our `.ENV` file. The username under `MONGOUSER` and the password under `MONGOPASS`.
|
||||
|
||||
Once this is done, we'll want to go back to the homepage of the Atlas cluster. You should then see a button labeled "Connect". Press that to start the instructions for how to connect our Node code to MongoDB.
|
||||
|
||||

|
||||
|
||||
This will bring up the dialog for the cluster. You'll see different connection options for Mongo Shell, Compass, or various drivers. Since we'll be using the NodeJS driver to connect our code, we'll select "Connect Your Application".
|
||||
|
||||

|
||||
|
||||
This will bring up a dialog where you can select the _Node.JS_ driver. This will give you the connection string with `<username>` and `<password>` that you'll need to replace with the credentials we created earlier.
|
||||
|
||||

|
||||
|
||||
This string is called the _connection string_ which is [a URI](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier). This URI will be used to connect your code to the database you just created. Let's store that string [in a template literal, which will allow us to interpolate variables into the string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) for the password:
|
||||
|
||||
```javascript
|
||||
const uri = `mongodb+srv://${mongoUser}:${mongoPass}@cluster0-xxxxx.mongodb.net/test?retryWrites=true&w=majority`;
|
||||
```
|
||||
|
||||
Now that we understand the URI we need to pass to the Node driver to connect to the database, we'll dive into the code we need to change to enable MongoDB.
|
||||
|
||||
## The Code {#mongodb-code}
|
||||
|
||||
```javascript
|
||||
const { createEventAdapter } = require('@slack/events-api');
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
const { MongoClient } = require('mongodb');
|
||||
const { tablize } = require('batteries-not-included/dist/utils/index.js');
|
||||
|
||||
const token = process.env.OAUTH_TOKEN;
|
||||
const slackSigningSecret = process.env.SLACK_SIGNING_SECRET;
|
||||
// Grab the MongoDB password and username we stored in our env file
|
||||
const mongoPass = process.env.MONGOPASS;
|
||||
const mongoUser = process.env.MONGOUSER;
|
||||
const port = process.env.PORT || 3000;
|
||||
const uri = `mongodb+srv://${mongoUser}:${mongoPass}@cluster0-xxxxx.mongodb.net/test?retryWrites=true&w=majority`;
|
||||
|
||||
const slackEvents = createEventAdapter(slackSigningSecret);
|
||||
const web = new WebClient(token);
|
||||
const dbClient = new MongoClient(uri, { useNewUrlParser: true });
|
||||
|
||||
// Connect to Mongo server instance
|
||||
dbClient.connect(err => {
|
||||
// Show any errors that showup in the
|
||||
if (err) console.error(err);
|
||||
// Connect to the test database in a cluster. Connect to the scores collection in that database
|
||||
const collection = dbClient.db('test').collection('scores');
|
||||
|
||||
const getIsPlusOrMinus = str => {
|
||||
const plusOrMinusRegex = /\@(\w+?)(\-{2}|\+{2}|\—{1})/;
|
||||
const [_, itemToScore, scoreStr] = plusOrMinusRegex.exec(str) || [];
|
||||
switch (scoreStr) {
|
||||
case '--':
|
||||
case '—':
|
||||
return { action: 'minus', word: itemToScore };
|
||||
case '++':
|
||||
return { action: 'add', word: itemToScore };
|
||||
default:
|
||||
return { action: '', word: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
slackEvents.on('message', async event => {
|
||||
try {
|
||||
console.log(`Received a message event: user ${event.user} in channel ${event.channel} says ${event.text}`);
|
||||
|
||||
const { action, word } = getIsPlusOrMinus(event.text);
|
||||
if (action) {
|
||||
const value = action == 'add' ? 1 : -1;
|
||||
|
||||
// Update the document and also return the document's value for us to use
|
||||
const doc = await collection.findOneAndUpdate(
|
||||
{ word },
|
||||
// Add `value` to "count" property. If `-1`, then remove one from "count"
|
||||
{ $inc: { count: value } },
|
||||
// `returnOriginal: false` says to return the updated document
|
||||
// `upsert` means that if the document doesn't already exist, create a new one
|
||||
{ returnOriginal: false, upsert: true }
|
||||
);
|
||||
|
||||
const actionString = action == 'add' ? 'had a point added' : 'had a point removed';
|
||||
|
||||
const result = await web.chat.postMessage({
|
||||
text: `${doc.value.word} ${actionString}. Score is now at: ${doc.value.count}`,
|
||||
channel: event.channel,
|
||||
});
|
||||
|
||||
console.log(`Successfully send message ${result.ts} in conversation ${event.channel}`);
|
||||
}
|
||||
|
||||
if (/@pointsrus leaderboard/i.exec(event.text)) {
|
||||
const topTenCollection = await collection
|
||||
// Find ANY document
|
||||
.find({})
|
||||
// Sort it from highest to lowest
|
||||
.sort({ count: 1 })
|
||||
// Limit it to 10 in case there are hundreds of values
|
||||
.limit(10)
|
||||
// Then, return it as a promise that has an array in it
|
||||
.toArray();
|
||||
// Mapping the array to display with `tablize`
|
||||
const state = topTenCollection.map(doc => {
|
||||
return [doc.word, doc.count];
|
||||
});
|
||||
const tableString = tablize([['Item', 'Count'], ...state]);
|
||||
|
||||
const result = await web.chat.postMessage({
|
||||
text: '```\n' + tableString + '```',
|
||||
channel: event.channel,
|
||||
});
|
||||
|
||||
console.log(`Successfully send message ${result.ts} in conversation ${event.channel}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
slackEvents.on('error', console.error);
|
||||
|
||||
slackEvents.start(port).then(() => {
|
||||
console.log(`server listening on port ${port}`);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
If you do a diff against the previous code, you'll see that we were able to add the database using only 4 or 5 new operations. These operations are to:
|
||||
|
||||
- Connect to the Mongo driver
|
||||
- Create a new connection to the database
|
||||
- An update and get query
|
||||
- A find query to list the leaderboard
|
||||
|
||||
Because we now have a database running the data show, we can be sure that our data will persist - even if or when our server goes down (either for maintenance or a crash). Now that we have the code updates, let's get to deploying the code we had set up.
|
||||
|
||||
# Deployment {#deployment}
|
||||
|
||||
Ideally, since our Slack app is a small side project, we'd like to host things in a straightforward manner for cheap/free. One of my favorite hosting solutions for such projects is [Heroku](heroku.com/). Heroku is no stranger to Slack apps, either. They have [an official blog post outlining making their own Slack bot using the web notification feature within Slack](https://blog.heroku.com/how-to-deploy-your-slack-bots-to-heroku). That said, our route is going to be a bit different from theirs because we chose to use the events subscriptions instead.
|
||||
|
||||
Let's start our step-by-step guide immediately after [you've created an account with Heroku](https://signup.heroku.com/).
|
||||
|
||||
Once you're logged in, you should see a dashboard like the following:
|
||||
|
||||

|
||||
|
||||
Once you see this page, select "New", then "Create new app".
|
||||
|
||||

|
||||
|
||||
This should let you provide a name for your project. This name should be memorable, since it will be used to generate the subdomain on Heroku's servers. For example, my `points-r-us` app is available at [points-r-us.herokuapp.com/](https://points-r-us.herokuapp.com/). While ultimately it doesn't matter much for a simple Slack bot, if you wanted to use this subdomain for other things later on that you might add on, it helps to have a memorable name.
|
||||
|
||||
Once this is done, open the Heroku app you just created by selecting it. You should see a dashboard screen like this:
|
||||
|
||||

|
||||
|
||||
The instructions that will show up While we'll be following these instructions shortly, we'll first want to setup our environment variables, just as we did with our `.env` file locally. You should see a "Settings" tab at the top of your dashboard.
|
||||
|
||||

|
||||
|
||||
Upon opening the tab, you should see a button labeled "Reveal Config Vars". Press the button and copy your environment variables from your `.env` file into the fields available.
|
||||
|
||||

|
||||
|
||||
Now that we have that, we can go back to our instructions that were on the main dashboard. Let's open up the same folder we have our `package.json` and `index.js` in, and install the Heroku CLI:
|
||||
|
||||
```
|
||||
npm i -g heroku
|
||||
```
|
||||
|
||||
> It's worth noting that Heroku officially suggests using [an alternative installation method for the CLI](https://devcenter.heroku.com/articles/heroku-cli) due to Node.JS incompatibilities, but I've faced no such issues with my (admittedly limited) usage.
|
||||
|
||||
Once this is done, we can:
|
||||
|
||||
- Sign into our account using `heroku login`
|
||||
|
||||
- Initialize a git repo into this folder `git init`
|
||||
|
||||
- Add Heroku as a remote place to deploy to. We should be able to see our Git URL from the settings page, so for example I would run:
|
||||
|
||||
```
|
||||
git remote add heroku https://git.heroku.com/points-r-us.git
|
||||
```
|
||||
|
||||
Now that we have Heroku set up, we're able to `git push heroku master` to have Heroku deploy our `npm start` script. This means that anything we put in our `package.json`'s `start` `script` property, then commit and push, will then be run on our server. As such, the first thing we need to do is verify that we own that subdomain for Slack to send events to.
|
||||
|
||||
While our `package.json` might have looked like this before:
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"start:dev": "env-cmd node ./index.js",
|
||||
"start": "node ./index.js",
|
||||
},
|
||||
```
|
||||
|
||||
We'll want to update it so that the `start` command uses the signing secret from our server environment variables to verify:
|
||||
|
||||
```json
|
||||
"verify": "slack-verify --secret $SLACK_SIGNING_SECRET --port=$PORT",
|
||||
"start": "npm run verify",
|
||||
```
|
||||
|
||||
We need to allow Heroku to dictate the port to host our verification command as well, to get past their firewall they automatically route to the app's subdomain; hence the `--port` attribute.
|
||||
|
||||
After making this change, we'll run:
|
||||
|
||||
- `git commit -m "Enforced verification"`
|
||||
- `git push heroku master`
|
||||
|
||||
And watch as our app gets deployed:
|
||||
|
||||

|
||||
|
||||
After this, we can go back to the Slack app dashboard and change the Event Subscription URL.
|
||||
|
||||

|
||||
|
||||
> Don't forget to hit "Save" once you change the URL 😉
|
||||
|
||||
Finally after this change is made, you can modify your `package.json` to run the server with `node` once again:
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"start": "node ./index.js",
|
||||
},
|
||||
```
|
||||
|
||||
> Be sure to use `node` and not `env-cmd`, as we want to actually use the values from the environment variable, not from a `.env` file.
|
||||
|
||||
Run that last `git commit` and `git push heroku master` and congrats! You should have everything deployed and ready to use!
|
||||
|
||||

|
||||
|
||||
# Conclusion {#conclusion}
|
||||
|
||||
Slack provides a feature-rich, very useful chat application. Being able to add in your own functionality to said application only makes things more powerful for either your group or your end users. I know many businesses will use Slack bots as another experience for their business users. Now you've been able to see the power of their Node SDK and how easy it is to setup and deploy your very own Slack app using MongoDB and Heroku!
|
||||
|
||||
Any questions or comments we didn't touch on here? Let us know down below or [in our Discord](https://discord.gg/FMcvc6T) where you can ask questions in real time with folks from our community!
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 104 KiB |
BIN
content/blog/making-a-slack-bot-with-node-and-mongo/showcase.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 66 KiB |