Git Product home page Git Product logo

blog's People

Contributors

gedayede avatar gefeiyanga avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

blog's Issues

防抖

防抖

需求

移动端上滑触底,加载下一页数据

为啥需要防抖

1. 性能:此处没有必要时时刻刻监听滚动事件

2. 用户滑动屏幕过程中,Safari浏览器不执行js代码,用户停止滑动操作后,方可执行业务代码。

3. 在满足条件的时间里,用户滑动过程中会多次触发请求,需用户停止滑动一段时间后判断是否触发请求。

代码部分:

...

componentDidMount () {
    // 200ms内只监听一次滚动事件
    window.addEventListener('scroll', this.debounce(this.handleScroll, 200))
}

...

// 防抖
debounce = (fun, delay) => {
    return (args) => {
        clearTimeout(fun.id)
        fun.id = setTimeout( () => {
            fun.call(this, args)
        }, delay)
    }
}
// 滚动事件
handleScroll = async(e) => {
    // 可视区域高度
    let clientHeight = document.documentElement.clientHeight;
    // 滚动条滚动高度
    let scrollTop  = document.documentElement.scrollTop || document.body.scrollTop;
    // 滚动内容高度
    let scrollHeight =document.documentElement.scrollHeight;
    if(clientHeight + scrollTop > scrollHeight - 1) {
        ...
    }
}

...

avatar

触底加载下一页的触发条件

可视区域高度 加 滚动条滚动高度 和 滚动内容高度 的大小关系
clientHeight + scrollTop scrollHeight

判断是否是微信浏览器

判断是否是微信浏览器

代码部分:

isWechat = ()=> {
    let ua = navigator.userAgent.toLowerCase();

    if (ua.match(/MicroMessenger/i) == "micromessenger") {
        return true
    } else {
        return false
    }
}

React onClick

// React中:
<button onClick={this.handleClick}>
  ...
</button>
// 相当于原生js中
<button onclick="handleClick()">
  ...
</button>

// React 可能需要绑定this(取决于是类组件还是函数组件)
// 如果是类组件,绑定this的方式

// 第一种 参考 https://zh-hans.reactjs.org/docs/handling-events.html
constructor(props) {
  ...
  // 为了在回调中使用 `this`,这个绑定是必不可少的
  this.handleClick = this.handleClick.bind(this);
}
...
handleClick() {
  this.setState(state => ({
    isToggleOn: !state.isToggleOn
  }));
}
...
render() {
  return (
    <button onClick={this.handleClick}>
      ...   
    </button>
  );
}

// 第二种 箭头函数,也方便传参数
handleClick = () =>  {
  // 写成箭头函数是为了不使用bind也能拿到this
  this.setState(state => ({
    isToggleOn: !state.isToggleOn
  }));
}
<button onClick={() => this.handleClick()}>
  ...
</button>
// 第二种的 onClick={() => this.handleClick()}
// 实际上是
xxx = () => {
  // 下面代码能用this
}

前端框架技术选型,Vue/React

vue 对比 react:

对比vue2和vue3,vue3的组合式API无疑是更好的选择,以下优势针对于vue3版本而言;react也仅指使用create react app方式构建的项目

  • 优势
  1. 使用vite搭建vue3项目框架,自带路由管理,sass,less支持的很好,yarn装一下包即可使用,无需多余配置;react需要手动配置各种loader
  2. 组合式API,逻辑抽离方便,不再受限于生命周期函数;react也可以做到
  3. vite使用Rollup打包,速度大幅优于webpack,开发体验更好;react大多使用webpack,打包速度一般
  • 劣势
  1. vue不支持IE11(reactive使用的是Proxy,IE全系不支持),react进行相关配置后支持IE9-IE11
  2. vue TS支持不够好;react天生支持
  3. vue2升级vue3有代价,社区相关轮子未必及时适配;react社区活跃度高,大版本升级更早,适配大概更全

总的来说,react就是f(state) = UI,仅仅是一个UI框架,其他的交给开发者自己配置;而vite生成的vue项目是一个功能更全的脚手架,帮开发者做了很多其他事(比如路由)。

docker run 传参数自定义前端请求proxy

.Dockerfile

# Base on offical Node.js Alpine image
FROM node:alpine

# Set working directory
WORKDIR /usr/app

# Install PM2 globally
RUN npm install --global pm2

# Copy package.json and package-lock.json before other files
# Utilise Docker cache to save re-installing dependencies if unchanged
COPY ./package*.json ./

# Install dependencies
RUN npm install --production


# Copy all files
COPY ./ ./


# Build app
RUN npm run build

# Expose the listening port
EXPOSE 8008

# Run container as non-root (unprivileged) user
# The node user is provided in the Node.js Alpine base image
USER node

# RUN sed -i "s/TRANSPORT_URL/$TRANSPORT_URL/g" ./next.config.js;

# Run npm start script with PM2 when container starts
CMD  sed -i "s/TRANSPORT_URL/$TRANSPORT_URL/g" ./next.config.js; npm run build; "pm2-runtime" "npm" "--" "start"

docker build

docker build -t chatgpt-web:test .

docker run

// 注意使用"\\" 对字符 "/"进行转义
// 该命令输出sha256id
// chatgpt-web
docker run -u root -p 8008:8008 -d -e TRANSPORT_URL=159.1.32.95:3000\\/chat chatgpt-web:test
// chatgpt-transport
docker run -u root -p 3000:3000 -d -e OUTER_URL=192.168.0.75:12710 chatgpt-transport:test

查看logs

// 3分钟后左右查看日志,若出现: ready - started server on 0.0.0.0:8008, url: http://localhost:8008, 则说明前端项目重新打包并且启动完成
// 1分钟左右后查看日志,若出现:  Nest application successfully started, 则说明后端项目重新打包并且启动完成
docker logs --since 5m sha256id

centos 8部署ChatGPT

下载安装node v18.10.0

wget https://nodejs.org/dist/v18.10.0/node-v18.10.0-linux-x64.tar.xz
tar -xvf node...tar.xz
mv node-v18.0.0-linux-x64  nodejs 
ln -s /where/nodejs/bin/npm /usr/local/bin/ 
ln -s /where/nodejs/bin/node /usr/local/bin/    // where指该文件所在路径
node -v   // 验证

安装nestjs

npm i -g @nestjs/cli

安装pm2

npm install pm2@latest -g
find / -name pm2
ln -s /where/bin/pm2 /usr/local/bin/    // where指该文件所在路径

安装yarn

npm install --global yarn
find / -name yarn
ln -s /where/bin/yarn /usr/local/bin/    // where指该文件所在路径

安装git

yum install curl-devel expat-devel gettext-devel \
 openssl-devel zlib-devel
yum -y install git-core
git --version    // 验证
配置.ssh_key

拉取代码并打包

git clone http://coderepo.com/projectname.git
cd projectname
yarn
yarn build

pm2启动

pm2 start dist/main.js --name <application_name>

本地存储历史记录

本地存储历史记录

需求

需要记录搜索记录,和后端同学交流后,决定浏览器本地记录数据

方案一 localStorage

localStorage可以存储5M左右的数据,且永久保存,存取方便

代码部分:

componentDidMount () {
    // 取值,记得倒序
    let history = localStorage.getItem('history') ? localStorage.getItem('history').split('+') : []
    ...
}

// 跳转搜索方法
goSearchPage () => {
    ...
    // 存值
    localStorage.setItem('history', localStorage.getItem('history') ? `${localStorage.getItem('history')}+${searchValue}` : searchValue )
    ...
}

后来因为seo优化,四个频道拥有独立的子域名,localStorage无法满足跨域存取,上网搜索后可以通过postmessage方案解决,但时间比较紧张,解决方法看起来比较复杂,遂放弃,转向方案二

方案二 cookie

cookie只能皴4k左右的数据,一个汉字占两个字节,故能存取2000个左右汉字,关键是cookie能够跨域存取(二级域名要相同)

代码部分:

componentDidMount () {
    // 取值,记得倒序
    let history = !getCookie('mobileHistory')||isEmpty(getCookie('mobileHistory')) ? [] : [...JSON.parse(decodeURI(getCookie('mobileHistory')))]
    ...
}

// 跳转搜索方法
goSearchPage () => {
    ...
    // 存值
    let domain = '.znzmo.com'
    let oldCookie = getCookie('mobileHistory') ? decodeURI(JSON.parse(getCookie('mobileHistory'))) : {}
    if (!getCookie('mobileHistory')||isEmpty(oldCookie)) {
        setCookie('mobileHistory', JSON.stringify([encodeURI(searchValue)]), 100000, domain)
    } else {
        // 只存最新的10个
        if (oldCookie.split.length>10) {
            setCookie('mobileHistory', JSON.stringify([...map(oldCookie.split(',').slice(-10), (item)=> encodeURI(item)), encodeURI(searchValue)]), 100000, domain)
        } else {
            setCookie('mobileHistory', JSON.stringify([...map(oldCookie.split(','), (item)=> encodeURI(item)), encodeURI(searchValue)]), 100000, domain)
        }
    }
    ...
}

实际开发中,遇到了一个非常🐶的问题,本地调的没问题了,测试环境用Chrome的手机模拟器使用也没问题,但到手机端直接报错。装了vconsole后,打印出来,提示cookie取值出了问题,再去网上搜一波,得知Safari不能存储非ASCII码的数据。故不得不在存值的时候encodeURI,在取值的时候decodeURI,解决。

附上封装的存取方法

// 设置cookie
export const setCookie = (cname, cvalue, exdays, domain) => {
	var d = new Date();
	d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
	var expires = "expires=" + d.toUTCString();
	domain = domain ? `domain = ${domain};` : ''
	document.cookie = `${cname}=${cvalue};${expires};${domain}path=/`
}

// 获取cookie
export const getCookie = (cname) => {
	var name = cname + "=";
	var ca = document.cookie.split(';');
	for (var i = 0; i < ca.length; i++) {
		var c = ca[i];
		while (c.charAt(0) == ' ') {
			c = c.substring(1);
		}
		if (c.indexOf(name) == 0) {
			return c.substring(name.length, c.length);
		}
	}
	return "";
}

Flutter 记录

屏幕宽度百分比:MediaQuery.of(context).size.width * per

row均分

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  mainAxisSize: MainAxisSize.max,
  //交叉轴的布局方式,对于column来说就是水平方向的布局方式
  crossAxisAlignment: CrossAxisAlignment.center,
  //就是字child的垂直布局方向,向上还是向下
  verticalDirection: VerticalDirection.down,
  children: <Widget>[
    Text('我是第1个'),
    Text('我是第2个'),
    Text('我是第3个'),
  ],
)

全局监听点击空白区域收起键盘

  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      builder: (context, child) => Scaffold(
        body: GestureDetector(
          onTap: () {
            FocusScopeNode currentFocus = FocusScope.of(context);
            if (!currentFocus.hasPrimaryFocus &&
                currentFocus.focusedChild != null) {
              FocusManager.instance.primaryFocus.unfocus();
            }
          },
        ),
      ),
      home: DismissKeyboardDemo(),
    );

解决键盘弹起引发内容溢出的bug

body: SingleChildScrollView(
  physics: new NeverScrollableScrollPhysics(),  // 阻止滚动
  child: ConstrainedBox(
      constraints: new BoxConstraints(
           minHeight: 300
      ),
      child: Column(
          children: <Widget>[]
      )
)

给任意元素添加点击事件

InkWell(onTap: null, child: null)  // 包裹的任意区域都可以触发点击
GestureDetector(onTap: null, child: null)  // 包裹的有内容的区域可以触发点击

删除iOS编译文件,重新生成

flutter create -i swift .

base64转PDF 插件:

import 'dart:io';
import 'dart:convert';
import 'package:flutter_full_pdf_viewer/full_pdf_viewer_scaffold.dart';  // full_pdf_viewer_scaffold
import 'dart:async';
import 'package:path_provider/path_provider.dart';
...
  @override
  void initState() {
    asyncInit(202011061615348).then((f) {
      print(f.path);
      setState(() {
        pathPDF = f.path;
      });
    });
    super.initState();
  }
...
Future<File> asyncInit (id) async {
    var filename=id;
    var pdfBase64 = await WorkOrderServe().getReport(id);
    var pdfDataBytes = base64.decode(pdfBase64);
    String dir = (await getApplicationDocumentsDirectory()).path;
    File file = new File('$dir/${filename}.pdf');
    await file.writeAsBytes(pdfDataBytes);
    return file;
  }
...
Build:
    if (pathPDF!='') {
      print(11);
      return PDFViewerScaffold(
        appBar: AppBar(
          title: Text('诊断报告'),
          backgroundColor: Color(int.parse('16212E',radix:16)).withAlpha(255),
        ),
        path: pathPDF,
      );
    }

Unimplemented handling of missing static target 报错

Flutter Hot Restart 或者重启

修改.gitignore后

git rm -r --cached . 
git add .

git commit -am "Remove ignored files"

git pull origin branch-name
git push origin branch-name

项目启动报错 Error connecting to the service protocol: failed to connect to http://127.0.0.1:49855/tUl8jpG1mhw=/

这是因为挂了代理,在~/.bash_profile中加export NO_PROXY=localhost,127.0.0.1,然后source 它,然后重启电脑。
如果是zsh,则加在~/.zshrc

📦 .apk后不能掉接口

在android/src/main/AndroidManifest.xml中添加:

...
</application>

// 新增代码
<uses-permission android:name="android.permission.READ_PHONE_STATE" />  // 访问电话状态
<uses-permission android:name="android.permission.INTERNET" />  // 允许全部网络访问
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />  // 获取网络信息状态
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />  // 获取当前WiFi接入的状态以及WLAN热点的信息
...

📦 .apk方法 https://www.jianshu.com/p/fabcfd621e01

📦.apk报错 Could not determine the dependencies of task ':app:compileReleaseJavaWithJavac

在 工程名/android/bulid.gradle文件中修改


allprojects {
    repositories {
//        google()
//        jcenter()
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
      
        maven { url 'http://download.flutter.io' }
        maven { url "https://storage.googleapis.com/download.flutter.io" }
    }
}

Unhandled Exception: inheritFromWidgetOfExactType(_LocalizationsScope) or inheritFromElement() was called before _ScreenState.initState() completed

解决方法:把ininstate中的逻辑放到Future.delayed()中,或者放到didChangeDependencies生命周期中

输入格式限制代码如下,且需要 import 'package:flutter/services.dart';

  inputFormatters: [
    // 只能输入数字和英文字母
    WhitelistingTextInputFormatter(
      RegExp(
        "[a-zA-Z]|[0-9]"
      ),
    ),
    LengthLimitingTextInputFormatter(20),  // 长度限制
  ],

自定义TabBar

Container(
  decoration: BoxDecoration(
    border: Border(bottom: BorderSide(color: Color(0xFFEEEEEE), width: 1))
  ),
  child: TabBar(
    controller: _tabController,
    unselectedLabelColor: Color(0xFF74787C),
    labelColor: Color(0xFF0860AB),
    indicatorColor: Color(0xFF0860AB),
    labelStyle: TextStyle(fontSize: 16),
    indicator: UnderlineTabIndicator(
      borderSide: BorderSide(width: 1.5),
      insets: EdgeInsets.symmetric(horizontal: 34.0),  // 自定义指示器的size
    ),
      tabs: [
            Tab(
              child: Container(
                height: 22,
                child: Text('进度处理'),
              ),
            ),
            Tab(
              child: Container(
                height: 22,
                child: Text('故障分析'),
              ),
            ),
            Tab(
              child: Container(
                height: 22,
                child: Text('维修反馈'),
              ),
            )
    ]
  ),
)

TabBarView报错

给Tabview外层包一个有宽高的容器

Container(
  width: MediaQuery.of(context).size.width,
  height: MediaQuery.of(context).size.height,
  child: TabBarView(
    controller: _tabController,
    children: [
      ...
    ]),
),

顶部固定,底部固定的布局

class _WorkOrderDetailState extends State<WorkOrderDetail> with TickerProviderStateMixin {

  final _topPartNotifier = ValueNotifier<int>(0);

  GlobalKey tabsKey = GlobalKey();
  TabController _tabController;


  @override
  void initState() {
    _tabController = TabController(length: 3, vsync: this);

    () async {
      await Future.delayed(Duration.zero);
      RenderBox renderBox = tabsKey.currentContext.findRenderObject();  // 获取TabBar对象
      _topPartNotifier.value = renderBox.size.height.toInt() + MediaQueryData.fromWindow(window).padding.top.toInt() + kToolbarHeight.toInt();  
      // TabBar高度 + 状态条高度(需要 import 'dart:ui';) + appBar高度
      // import 'dart:ui';  拿 MediaQueryData.fromWindow(window)
    }();

    super.initState();
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('工单详情'),
    ),
        body: SingleChildScrollView(
          padding: EdgeInsets.only(bottom: 0),
          physics: new NeverScrollableScrollPhysics(),  // 阻止滚动
          child: ConstrainedBox(
              constraints: new BoxConstraints(
                  minHeight: 300
              ),
              child: Column(
                children: [
                  Container(
                    key: tabsKey,
                    child: TabBar(
                        controller: _tabController,
                        ...
                        tabs: [
                          Tab(),
                          ...                        ]
                    ),
                  ),
                  ValueListenableBuilder(
                    valueListenable: _topPartNotifier,
                    builder: (_, value, __) => Container(
                      width: MediaQuery.of(context).size.width,
                      height: MediaQuery.of(context).size.height - value,  
                      // TabBarView外层需要包一个固定大小的容器,其中
                      // 高度 = 屏幕高度 - 顶部固定高度(状态条高度、appBar高度、TabBar高度);
                      // TabBarView 子组件中再使用一次SingleChildScrollView布局,
                      // 底部固定需要再使用Stack, Positioned 布局
                      child: TabBarView(
                          controller: _tabController,
                          children: [
                            DetailProgress(),
                            FailureAnalysis(),
                            RepairFeedback(),
                          ]),
                    ),
                  ),
                ],
              ),
          )
      )

    );
  }
}

initState 拿不到context

void initState() {
  () async {
    await Future.delayed(Duration.zero);
    ... use(context, ...)      
  }();

}

报错 Type 'List' is not a subtype of type 'Widget'

解决方法:

children: ...List<Widget>

获取当前时间戳 DateTime.now().millisecondsSinceEpoch

flutter_local_notifications iOS端不能使用

在AppDelegate.swift文件中(使用vs code搜索关键词)加一些代码

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    // 增加的部分
    if #available(iOS 10.0, *) {
      UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
    }
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

判断手机系统平台

import 'dart:io' show Platform;

if (Platform.isAndroid) {
  // Android-specific code
} else if (Platform.isIOS) {
  // iOS-specific code
}

flutter 抓包

static void init() {
    // 在调试模式下需要抓包调试,所以使用代理,并禁用HTTPS证书校验
    if(!Global.isRelease) {
      (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
        (client) {
          client.findProxy = (uri) {
            return "PROXY localhost:8866";
          };
          // 代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以禁止证书校验
          client.badCertificateCallback =
            (X509Certificate cert, String host, int port) => true;
        };
    }
}

无context使用Provider https://github.com/fluttercommunity/get_it

Could not open settings remapped class cache for 3n8ygcxrc7nelqcw5tzib8bo6

https://stackoverflow.com/questions/49758849/flutter-io-android-license-status-unknown

Unable to find bundled Java version on Flutter

https://stackoverflow.com/questions/51281702/unable-to-find-bundled-java-version-on-flutter

Flutter Insecure http is not allowed by platform

https://stackoverflow.com/questions/64172791/flutter-insecure-http-is-not-allowed-by-platform

iPhone新机型吸底适配

iPhone新机型吸底适配

问题:

iPhoneX 及后来的苹果机型取消了物理按键,改成底部小黑条,导致常规吸底效果体验很差。

解决方案:

解决方法分三步
1. 设置网页在可视窗口的布局方式
viweport-fit 属性,使得页面内容完全覆盖整个窗口:

avatar

contain: 可视窗口完全包含网页内容(左图)
cover:网页内容完全覆盖可视窗口(右图)
auto:默认值,跟 contain 表现一致
<meta name="viewport" content="width=device-width, viewport-fit=cover">
2. 页面主体内容限定在安全区域内
如果不设置这个值,可能存在小黑条遮挡页面最底部内容的情况
body {
    // 兼容 iOS < 11.2
    padding-bottom: constant(safe-area-inset-bottom);
    // 兼容 iOS >= 11.2
    padding-bottom: env(safe-area-inset-bottom);
}
3. fixed 元素的适配
注意,这个方案需要吸底条必须是有背景色的,因为扩展的部分背景是跟随外容器的,否则出现镂空情况。
// 可以通过加内边距 padding 扩展高度:
{
    padding-bottom: constant(safe-area-inset-bottom);
    padding-bottom: env(safe-area-inset-bottom);
}

// 或者通过计算函数 calc 覆盖原来高度:
{
    height: calc(60px(实际情况决定) + constant(safe-area-inset-bottom));
    height: calc(60px(实际情况决定) + env(safe-area-inset-bottom));
}

参考文档

吸顶

吸顶

代码部分:

...

componentDidMount () {
    // filterOffsetTop 目标元素初始位置距离顶部高度
    filterOffsetTop = document.getElementById('filter').offsetTop
    // 监听滚动事件
    window.addEventListener('scroll', this.ceiling)
}

...

ceiling = () => {
    //滚动条滚动高度
    let scrollTop  = document.documentElement.scrollTop || document.body.scrollTop;
    // headerHeight 目标元素吸顶时距离顶部的高度
    if(scrollTop + headerHeight > filterOffsetTop) {
        this.setState({
            isFixedNavTab: true,
        })
    } else {
        this.setState({
            isFixedNavTab: false,
        })
    }
}

...

render () {
    return (
        <div id='filter' className={!isFixedNavTab ? style.navTabWrap : [style.navTabWrap, style.fixedNavTabWrap].join(' ')}>
            ...
        </div>
    )
}

avatar

为了实现“平滑”的吸顶效果,需要
1. 获取精确的目标元素的初始高度

filterOffsetTop = document.getElementById('filter').offsetTop

2. 计算精确吸顶的临界状态

滚动条滚动高度 加 目标元素吸顶状态时距离顶部的高度(通常为0,实际需求确定) 和 目标元素初始高度 大小的关系,区分是否加载fixed样式类

scrollTop + headerHeight filterOffsetTop

解决第三方资源跨域和防盗链问题

解决跨域问题

资源中存在 crossorigin="anonymous",要去除,如

detailHtml?.replace(/crossorigin="anonymous"/g, '')

解决放到链问题

去除请求头中的referrer字段

<meta name="referrer" content="never">
// 如果不想去除所有请求头中的该字段,可动态修改 ,如
// 可在head中先不去除
<meta id="referrer" name="referrer" content="always" />
// 需要去除时去除
const referrer  = document.getElementById("referrer");
referrer.setAttribute("content", "never")

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.