Giter Club home page Giter Club logo

notes's Introduction

Welcome

Hi~ This is MadCcc, a frontend rookie 🐦.

github-status

notes's People

Contributors

madccc avatar

Watchers

 avatar

notes's Issues

如何从研发的角度提升网站SEO质量

前言

Search Engine Optimization: The process of improving the quality and quantity of website traffic to a website or a web page from search engines.

SEO对于一个想要在搜索引擎上获得较高排名的网站来说是必不可少的,那么如何提高网站的SEO质量呢?本文将会挑几点重要并且有效的详细讲解。

先上一些参考资料,详细的如何优化网站的SEO可以在这些资料中找到

技术选型

SEO是针对搜索引擎的优化,而搜索引擎在爬取站点的时候并不是以浏览器的方式,而是类似于curl的方式去爬取,所以我们必须要保证搜索引擎在访问我们的站点时能够更加快速有效的获取有效信息。

这里不推荐使用纯SPA框架,如简单的create-react-app或者vue-cli创建的项目。这些项目都是客户端渲染的(CSR,Client Side Rendering),这也就意味着你去curl这个网页只会得到一段毫无任何信息的HTML,所有的页面内容都是在本地(浏览器)运行js代码之后生成的。

如果你的站点都是静态内容,我会推荐使用一些支持SSG(Static Site Generation)的框架,比如Gasbyjs,这类框架可以在构建时将你的网站直接构建为一个完整的HTML,并部署到你的云平台上。这时候搜索引擎爬取你的站点时就会拿到有用的信息了。

如果你的站东动态内容较多,推荐使用支持SSR(Server Side Rendering)的框架,比如Nextjs或者Nuxtjs,它们分别基于React和Vue框架,并且也同时支持SSG构建。SSR会在用户访问站点时在后台生成完整的HTML,所以可以实时地获取一些动态数据。

Robots.txt

robots.txt文件可以有效的管理流向你的站点的流量,搜索引擎都会遵循站点下的robots.txt文件里制定的规则。robots.txt文件中可以指定某个搜索引擎的bot(如googlebot)是否可以访问你的站点下的某些路径,如果明令禁止了,搜索引擎将不会爬取你规定的站点路径。默认情况下所有的站点路径都可以被爬取。下面是一个例子:

# Group 1
User-agent: Googlebot
Disallow: /nogooglebot/

# Group 2
User-agent: *
Allow: /

Sitemap: http://www.example.com/sitemap.xml

这个简单的robots.txt文件禁止了googlebot爬取/nogooglebot/路径下的所有页面。同时还指定了这个站点下的sitemap的地址,可供搜索引擎获取sitemap

Sitemap

sitemap通常是一个xml文件,里面你可以提供所有你希望搜索引擎爬取的网页/视频等静态资源。有了sitemap,搜索引擎爬取你的网站时会更有指向性。下面是一个例子

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
  <url>
    <loc>http://example.com</loc>
    <changefreq>weekly</changefreq>
    <priority>1</priority>
    <lastmod>2021-09-29T09:14:23.422Z</lastmod>
  </url>
  <url>
    <loc>http://example.com/about</loc>
    <changefreq>weekly</changefreq>
    <priority>1</priority>
    <lastmod>2021-09-29T09:14:23.422Z</lastmod>
  </url>
</urlset>

上面的sitemap就提供了两个页面给搜索引擎,同时也可以指定一下其他的属性,比如changefreq代表这个页面的更新频率,priority代表这个页面的权重,lastmod代表这个页面的上次修改时间等等。

sitemap数量不宜过多,2000~3000条会是一个比较合适的数量。如果数量太多的话我们可以把sitemap下分到子路径中,如我们可以同时提供http://example.com/sitemap.xmlhttp://example.com/about/sitemap.xml

Meta信息

Meta信息包括网站的title/description/keywords,其中google已经明确表示不会对keywords收集信息,所以可以忽略。一个SEO友好的站点,所有页面都需要有精准的meta信息,用于存放一些关键词与这个页面的主要内容。

我们需要把这些meta信息放在网页的<head>标签中,例如vue的官网首页:

<title>Vue.js</title>
<meta name="description" content="Vue.js - The Progressive JavaScript Framework">

Internal links

Internal links,内链,特指那些可以链接到你的站点内其他页面的链接(a标签)。这些链接会被搜索引擎记录并爬取,所以非常有利于构建站点的拓扑结构。因此在网站中加入大量内链是可以提升网站整体的SEO质量的。

值得一提的是,现在广泛使用的前端框架如React和Vue都是有客户端路由的,它们也提供了两种跳转方式:命令式与声明式,以vue为例,vue-router提供了push等方法供用户在js代码中实现页面跳转,这些就是命令式,他们并不会在你的网页元素上留下a标签,所以这种方式对于SEO式不友好的。因此我们要尽量多地使用声明式的跳转,也就是利用框架提供的跳转组件,它们会为我们的网页实现SPA式的快速跳转,并且也留下带有信息的a标签在网页元素中。

Canonical Url

Canonical Url可以翻译为规范网址,常用于“合并同类项”。有以下几种场景我们可以用到规范网址:

  1. 明确指定你的网页在搜索引擎的搜索结果中展现的网址
  2. 合并相似的或者重复的但具有不同网址的网页
  3. 对于同一个网页降低流量跟踪的难度
  4. 方便管理联合内容
  5. 避免搜索引擎花太多时间爬取重复的网页

我们同样需要在head标签中指定规范网址,例如:

<link rel="canonical" href="https://example.com">

URL结构

URL就是一个网页的地址,那么对于一个SEO有好的网站,其内部所有网页都需要用URL分隔开来,以保证搜索引擎能够爬取到站点中的任何一个页面。

一个好的URL结构需要具备以下几条素质:

有意义的命名

也就是说就像为变量取名一样,你的URL中的每一段也必须是有意义的

❌  https://example.com/aaaaaaa
⭕️  https://example.com/about

反映网站结构

你的url同样可以反映你的站点的结构,所以你可以像建立文件夹一样为你的页面划分路径,比如:

https://example.com/article/how-to-use-github
https://example.com/article/my-first-repo-in-github

具备关键词

url中可以像title/description一样塞入一些关键词,但注意要保证url的简洁

https://example.com/awards
https://example.com/price

易读性

减少URL中查询参数/数字/符号的使用,提高URL的易读性

❌  https://github.com/MadCcc/Notes
⭕️  https://example.com/article?id=123456678

图片的Alt属性

对于搜索引擎的bot来说,图片只是一个元素而已,所以图片的alt属性的地位就变得很高,它代表了这个图片的意义,所以我们有必要在所有img元素上添加alt属性,并附上符合上下文的描述文字

Tab组件于Dropdown组件

构建页面的时候我们有时候会用到这两个组件,它们或者其他一些类似的组件都有一个共性:隐藏了一部分元素。因此隐藏的部分我们必须尤其注意,比如一些重要的信息如内链等,就会隐藏在这些组件中。

对于这种会隐藏信息的组件,我们在实现它们或者在选用其他社区提供的插件时,需要让他们能够以display: none的形式被隐藏,而非利用js的动态隐藏与生成,这样它们至少还存在于HTML中,可以被搜索引擎发现。

Tab组件我们则可以动态处理,利用SPA框架提供的feature,我们可以为每一个tab指定一个url,这样也可以让每一个tab下的内容被搜索引擎发现。

网站性能优化

网站性能同样是衡量SEO的重要标准,但这里就不展开讲了,我们可以在这个网站中了解更多:https://developers.google.com/speed/pagespeed/insights/

网页性能有三个比较重要的指标:Largest Contentful Paint (LCP)、First Input Delay (FID)、Cumulative Layout Shift (CLS),上面这个网站可以帮我们测量,这些可以作为我们优化网站的一个根据

总结

做SEO优化是一个非常零碎的工作,大部分答案其实都藏在google以及其他搜索引擎提供的文档中,属于是那种肯花时间阅读就一定能做好的活儿。但是从SEO优化的一些细碎标准中我们仍人可以学到很多平常会被我们忽略的小细节,所以细节在前端还是非常重要的。

React组件库探索

前言

也算是合理利用了闲时吧,研究了一下怎么搭建React的组件库,参考了antd和material-ui的项目结构以及编译方式,并且参考了一篇特别好的文章:《可能是最详细的React组件库搭建总结》,总算是把组件库这块拿下了,并且经过自己的一系列优化之后,取得了不错的效果,这里也算是记录一下自己的爬坑历程。

为什么要做组件库

可能很多人都是直接使用antd或者material-ui这种现成的组件库,这无疑是最省事且最安心的,前提是UI/UX必须得符合它们的设计。公司从toB偏向toC之后,产品对于UI的要求也逐渐升高,希望有自己的风格特色,并且希望所有的UI交互能够统一,最重要的是,UI设计会提出一些antd或者material-ui不能实现的交互方式,改造是可以改造,但拓展性不强,且改造过程艰辛容易出错,还依赖于目标组件库的版本。同时我们的前端团队也在不断的扩大,在产品已经超出一个小项目的时点,如何保证所有项目的UI风格统一,并且减少前端开发的不良体验,公司自己的组件库就应运而生了。

当我们在讨论组件库的时候,我们在讨论什么

所谓组件库,就是封装一些常用的小组件,比如Button/Icon等等。组件库内的所有组件都要有封装好的样式行为,这两点分别对应了我们的css文件和js文件。我们一般会使用一些css预处理器,比如sass;也会使用typescript来获得一个良好的编码体验。但是一个npm包其实是不应该提供ts和sass文件的,因为一个正常的前端项目一般是需要webpack提供dev环境以及打包辅助的,浏览器只认识js和css,所以webpack做的工作就是把我们的ts和sass文件编译为能够在浏览器中运行的js和css。我们知道webpack是通过loader去即时编译这些文件的,但是我们往往不会去让webpack编译node_modules里的内容,因为这样太消耗资源了,我们根本不知道有多少插件需要编译。所以我们在发布组件库的时候,需要提供编译后的js和css文件

组件库是一个很庞大的项目,包含组件、动画、工具库等等,但是再大项目也可以拆解成小的部分。我详细研究了antd和material-ui的项目结构,这两个组件库可以说构建的方式完全不同,但是项目组成却具有共性:一个存有所有小组件的components文件夹,其中每一个组件都有自己的样式文件,互不干扰;同时也有一些公共的样式,比如一些常用的颜色、边框等等;然后还会有一些用于辅助组件行为的工具库,以及常见的动画效果。所以我们要踏出的第一步就非常简单了:抛开其他无用的部分,单单实现一个简单的Button组件。

定下这个目标之后,我们就可以有目的地去研究antd和meterial-ui各自的具体实现了,随后再一步步地丰富组件库的内容,比如封装常用动画、添加主题等等。

技术选型

React的组件库相对来说比Vue的插件好做一些,不需要写什么setup函数,只需要提供以React编写的组件本身即可。我们会利用Typescript提升开发体验,因为ts提供了非常好的类型推断,可以减少代码中的语法错误发生概率。

而css预处理器就有很多选择了。sass、less以及react特色的styled component,还有jss,都是不错的选择。antd使用的是less,less和sass区别不大,都在原生的css体验上附加了很多新特性,管理和复用起来非常方便。jss和styled都是使用js编写样式代码的,这样带来的好处是方便管理颜色之类的变量,以及本身自带样式的按需加载。组件和样式的按需加载是组件库非常重要的一点,这样可以让每一个组件之间相互隔离,减少引用组件库后项目的打包体积。如果使用sass或者less还要实现按需加载的话就需要另外花一份功夫了。我们的组件库中会使用sass作为css预处理器,所以基本架构会和antd保持一致。

组件库还需要一份文档,不仅是为了给用户看,而且还是开发时进行调试的好方法。有很多选择可以在开发时在文档中引入组件,比如doczdumi。docz是一个基于Gasbyjs的文档框架,也就是说它会提供一个自动生成的基于Gasby的文档项目,我们只需要按照docz的要求,在mdx文件中引入我们开发中的组件,就可以即时地调试文档和组件了。dumi则是基于umi框架的一个组件库解决方案,它提供的cli甚至还提供了基于father的编译功能,可以帮助我们更快地构建组件库。

利用whistle对服务端请求进行抓包

前言

前端开发最重要的一环就是对接后端请求,正常情况下从浏览器发出的请求可以在浏览器的开发者工具的Network一栏看到。但对于服务端渲染的前端项目来说,大部分请求都是在服务端发出的,所以并不能在浏览的Network中监控到,所以我们会采用“抓包”的形式在开发过程中针对服务端请求进行监测。
常用的抓包工具有很多,比如Charles/ProxyMan等等,利用的都是代理(Proxy)将服务端请求代理到它们的端口上,再从它们的端口上发出,因此能在这些软件上看到从服务端发出的请求。但是Charles这些客户端软件对于内存占用却相当严重,因此本文介绍一个比较轻量的抓包工具:Whistle

安装方法

首先你需要安装Node.JS,因为Whistle是一个npm包,需要依赖nodejs运行。安装Node.JS这里就省略了。
然后我们开始安装whistle

npm install -g whistle

也可以用**的镜像源

npm install cnpm -g --registry=https://registry.npm.taobao.org
cnpm install -g whistle

or specify mirror install directly:

npm install whistle -g --registry=https://registry.npm.taobao.org

安装之后我们就可以直接运行whistle

whistle run -p 9090(或者其他你喜欢的端口号)

然后打开 http://localhost:9090 ,打开左侧的network一栏,就可以在这个页面监测代理到 http://localhost:9090 的所有请求了

image

Axios配置

这时候就有同学要问了,里面怎么啥也看不到啊!别急,我们还需要将服务端请求代理到whistle运行的端口上。这里以前端常用的Axios库为例:

axios.create({
  baseURL: ...(省略),
  timeout: 10000, // timeout range
  proxy: {
    host: 'localhost',
    port: 9090,
  },
});

这里就是将Axios的请求代理到whistle端口上,然后所有基于这个service发出的请求就都可以在whistle可视化界面中看到啦

结语

服务端渲染对于前端性能以及表现来说确实是一笔相当大的提升,但维护一个服务端渲染的项目却需要花费更多的精力。抓包只是服务端渲染在开发过程中的一个小问题,还有很多其他问题需要我们去理解与攻克。但出乎我意料的是nextjs这个框架更新频率超乎我的想象,这半年来竟然已经更新了一个大版本,可见这个团队对于框架维护还是非常勤劳的(尽管还是有一些小问题存在)。所以我坚信,没有解决不了的难题,所有我们花费的时间都将会得到回报!

搭建前端CLI模版

前言

一个完善的项目模版可以减少我们对于项目基建的重复性工作,在使用React的时候更加如此。
构建一个React项目常用的CLI(或者框架)有一些,我这里简单列举几个

  1. create react app
    React提供的 create react app 作为构建项目的起手未免有些过于简单了,只提供了Webpack这些使用React的最低限度配置,连Router都需要自己去配,有种我想造车但是炼铁技术都还没有成熟的感觉.
  2. Next.js
    Next.js 无疑是一个非常好用的基于React的SSR框架,优点有很多,比如甚至已经支持了ISG构建(Incremental Static Generation)。但引用Umi文档里的一句话,就是不够接地气。也就是说基于 Next.js 完成一个工程化的项目的话还是要做很多额外的配置

    next.js 是个很好的选择,Umi 很多功能是参考 next.js 做的。要说有哪些地方不如 Umi,我觉得可能是不够贴近业务,不够接地气。比如 antd、dva 的深度整合,比如国际化、权限、数据流、配置式路由、补丁方案、自动化 external 方面等等一线开发者才会遇到的问题。

  3. Umi
    Umi 确实很接地气,从构建一个初始项目就考虑了很多工程问题,比如支持两种路由(但不兼容):约定式路由和配置路由,支持项目内SSR直接配置以及SSR失败的降级方案。要说有什么不足的地方,可能就是使用人数还不够多,社区贡献会比较少,可能会有一些难以解决的小毛病。

上述三个只是一个构建项目"雏形"可以使用的技术,但是要融入一些我们自己的东西的话,比如集成Auth,初始化layout等,还是得在此之上做一些文章。

项目创建

所有伟大的项目都要从创建文件夹开始

# 创建文件夹
mkdir create-my-app && cd create-my-app

# 初始化npm仓库,一路回车即可
yarn init

我们会利用 nodejs 制作cli,所以我们需要一个安装这个npm包后可以被执行的command,去运行指定的js文件

# 创建bin文件夹
mkdir bin

# 创建入口文件
touch bin/create-my-app.js
// package.json

{
   ...,
   "bin": {
      "create-my-app": "./bin/create-my-app.js"
   },
   ...
}

之后我们还需要一个项目模版,用来拷贝构建

# 创建项目模版
mkdir templates && mkdir templates/ssr-nextjs

再创建一个src/文件夹,主要开发代码都会放在这里

# 创建src目录
mkdir src

现在我们整个目录结构会是这样的:

├──bin/
   └──create-my-app.js
├──src/
├──templates/
   ├──ssr-nextjs/
      └──...(template files)
└──package.json

实现CLI

处理命令行指令

我们可以使用 commander 处理命令行的指令,并且作为CLI的入口

# 安装typescript依赖
yarn add typescript ts-node @types/node -D

# 创建入口文件
touch src/cli.ts
// src/cli.ts

const { program } = require('commander');

program.version('0.0.1');

const handler = () => {
  // TODO: 做一些CLI该做的事情
}

program.action(() => {
  handler();
});

program.parse(process.argv);

commander其实是可以接受并处理一些命令行参数的,此处为一个简单实现,具体参照commander文档

实现用户交互

我们可以利用 chalkInquirer.js 实现一些简单的用户交互。
chalk 可以美化输出,比如给输出的文字指定一些颜色。
Inquirer.js 可以实现一些简单的用户输入,比如文字输入、选择等等。

我们先实现一个Generator类,用于项目构建

# 安装依赖,inquirer内含chalk依赖
yarn add inquirer
yarn add @types/inquirer -D

# 创建文件
touch src/generator.ts
// src/generator.ts

const fs = require('fs');
const chalk = require('chalk');
const inquirer = require('inquirer');

type AppOptions = {
  name: string;
  render: 'SSR' | 'CSR'
}

class AppGenerator {
  options: AppOptions;

  constructor() {
    this.options = {
      name: '',
      render: 'SSR',
    };
  }

  async init() {
    console.log(chalk.green('Starting create my app...'));
    console.log();
    await this.ask();
    this.write();
  }

  ask() {
    return inquirer.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project name:',
        default: 'new-my-app',
        validate(input: string) {
          if (fs.existsSync(input)) {
            return 'directory already exists';
          }
          return true;
        }
      },
      {
        type: 'list',
        name: 'render',
        message: 'Render Type:',
        choices: ['SSR', 'CSR'],
      }
    ]).then((answer: any) => {
      this.options.name = answer.name;
      this.options.render = answer.render;
    });
  }

  write() {
    // TODO: 这里做文件读写
  }

}

module.exports = AppGenerator;

inquirer.prompt接收一个数组作为参数,里面每一个元素都是提问用户的问题,可以设置这些问题的类型type、名称name(会作为答案的key)等等,并返回一个promise,第一个参数answer里就是所有用户输入/选择的内容,以一个Object的形式返回。
这里我们设置了两个问题,并将这两个问题的答案存储到这个AppGenerator Class里面。第一个问题是新项目的名称,我们会把这个名称填入package.jsonname字段中。第二个问题是项目的渲染方式,这将会决定我们给用户什么样的模版。这里还可以设置一些其他的问题,可以自行丰富。

这时我们可以用ts-node运行一下src/cli.ts,将会在命令行中展示给用户这两个问题,用户作答后程序就会退出

实现项目拷贝与模版填充

模版文件会放在templates/ssr-nextjs中,这里先不做展示了,我们先来看看如何将文件拷贝到用户的文件夹中去。这里我们用到的依赖如下:

  • ShellJs: 帮助我们模拟一些Linux指令,这里我们会用到cp指令拷贝文件
  • mem-fs-editor: 帮助我们实现文件读写,可以用于拷贝文件,也可以用于填充模版文件
# 安装依赖
yarn add shelljs mem-fs mem-fs-editor
yarn add @types/shelljs @types/mem-fs-editor -D
// src/generator.ts

// ...
const path = require('path');
const memFs = require('mem-fs');
const memFsEditor = require('mem-fs-editor');
const shell = require('shelljs');

// ...

class AppGenerator {
   options: AppOptions;
   tplDirPath: string;
   rootPath: string;
   fs: Editor;

   constructor() {
      const store = memFs.create();
      this.fs = memFsEditor.create(store);

      this.options = {
         name: '',
         render: 'SSR',
      };

      this.rootPath = path.resolve(__dirname, '../');
      this.tplDirPath = path.join(this.rootPath, 'templates/ssr-nextjs');
   }

   async init() {
      // ...
   }

   ask() {
      // ...
   }

   write() {
      this.buildTpl();
   }

   buildTpl() {
      const { name } = this.options;

      // 获取当前命令的执行目录,注意和项目目录区分
      const cwd = process.cwd();

      // 项目目录
      const projectPath = path.join(cwd, this.options.name);

      // 新建项目目录
      // 同步创建目录,以免文件目录不对齐
      fs.mkdirSync(projectPath);
      this.copyDir('*', projectPath);
      this.copyDir('.*', projectPath);

      // 使用存储的name答案填充模版中的空缺
      this.copyTpl('package.json', path.join(projectPath, 'package.json'), {
         name,
      });

      this.fs.commit(() => {
         console.log();
         console.log(`${chalk.grey(`创建项目: ${name}`)} ${chalk.green('✔ ')}`);
      });
   }

   getTplPath(file: string) {
      return path.join(this.tplDirPath, file);
   }

   copyTpl(file: string, to: string, data = {}) {
      const tplPath = this.getTplPath(file);
      this.fs.copyTpl(tplPath, to, data);
   }

   copy(file: string, to: string) {
      const tplPath = this.getTplPath(file);
      this.fs.copy(tplPath, to);
   }

   copyDir(dir: string, to: string) {
      const tplPath = this.getTplPath(dir);
      shell.cp('-r', tplPath, to);
   }

}

module.exports = AppGenerator;

其中package.json文件是一个ejs格式的模版文件,可以用以下方式留下空缺以待填充,这样最后生成的package.jsonname字段就是用户输入的内容了。

// templates/ssr-nextjs/package.json

{
   "name": "<%= name %>",
   ...
}

构建发布

到上一节为止一个CLI需要具备的功能我们都已经实现了,但细心的同学可能已经发现了,bin/create-my-app.js中还没有内容,那么这一步我们来做最后的工作。
我们使用father-build进行构建,偷个懒,不自己配babel或者rollup了,有兴趣的同学可以自行配置。

# 安装依赖
yarn add father-build rimraf -D

# 创建配置文件
touch .fatherrc.js
// .fatherrc.js

export default {
  entry: 'src/cli.ts',
  file: 'lib/cli.js',
  cjs: 'babel',
}

别忘了添加.gitignore,我们不应该把构建结果提交到git仓库上,lib/是我们的构建输出文件夹

node_modules/
.idea/

# build
lib/

然后让我们添加npm scripts

// package.json

{
   ...,
   "scripts": {
      "clean": "rimraf lib",
      "build": "npm run clean && father-build build",
   },
   ...
}

然后我们运行 yarn build 就可以看到我们的构建结果了

最后我们填充一下 bin/create-my-app.js

// bin/create-my-app.js

#!/usr/bin/env node

require('../lib/cli');

大功告成!代码已经构建完毕了,可以运行node bin/create-my-app.js庆祝一下了!
但先别急,还有最最最后一步操作,发布

# 安装np用于发布控制
yarn add np -D

np同时可以执行一些npm script作为hook,比如我们可以不用手动执行yarn build构建,而是让np在发布之前自动构建

// package.json

{
   ...,
   "scripts": {
      "clean": "rimraf lib",
      "build": "npm run clean && father-build build",
      "release": "np",
      "version": "npm run build"
   },
   ...
}

然后运行yarn release发布吧!前提是必须得有一个npm账号

使用CLI

在发布到npm仓库之后,我们就可以使用我们的CLI了,用法很简单,只需要运行一行短短的命令:

npx create-my-app

熟悉的场景再次出现在我们眼前!

总结

手撸CLI属实不易,还好找到一些很有参考价值的资料,并且参考create-umi-app的项目结构,这个CLI和这篇文章才由此诞生,感谢社区!
参考资料:

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.