xlkang / recording Goto Github PK
View Code? Open in Web Editor NEW存放一些记录和笔记,写在issues中
存放一些记录和笔记,写在issues中
最近入手一台ipad,打算尝试一下vscode
推出的在线开发服务。目前发布的vscode环境有微软官方的Visual Studio Codespaces
,GitHub Codespaces
,coding的cloud studio
等,官方和cloud studio
价格都不太美丽,GitHub Codespaces
还在内测,也不太理想。
后来发现了开源的code-server
,它也是专门为浏览器设计优化的 VS Code
,可以自行部署在自己的服务器上面,并且在任意浏览器中访问,另外ipadOS
上还有一款原生的VSApp
用来代替浏览器连接code-server
,它还专门进行了优化,相比浏览器中访问体验顺滑得多,因此决定用code-server
来搭建在线开发服务。
ipad一台
服务器一台,我用的阿里云ECS 2v4g,测试用只买了一周,系统是Ubuntu v18.04
可以连接到服务器的设备或者终端
下载code-server
gzip包(linux, 3.1.1) 下载地址,这里说一句,刚开始直接在服务器上用curl -o
命令下载下来,解压报错,原因是这个链接指向的其实不是gzip文件,而是下载链接,于是在github手动下载gzip包并且上传到服务器。
上传,这里我在公司的mac上用的FileZilla
,登录远程服务器,上传gzip包到/root目录下
解压到当前目录并进入文件夹
tar -xf FILE_NAME
cd FILE_NAME
在服务器上面设置名为 PASSWORD
的环境变量,后面 code-server
启动后就会直接读取这一环境变量并将之作为你的登录密码
export PASSWORD="YOUR_CODE_SERVER_PASSWORD"
目录中code-server
是可执行文件,可以直接在linux
下执行,接下来在服务器上启动code-server
./code-server --host "0.0.0.0"
code-server
进程就在远程服务器上启动并开始监听 8080
端口了,同时使用自定义的密码(YOUR_CODE_SERVER_PASSWORD
)作为认证密码。
最后打开这个端口的防火墙,在阿里云ECS服务器实例控制台中,在本实例安全组中添加开放8080
端口的入网授权策略(允许外网访问8080
端口),因为我的code-server
服务可能会被不同的ip访问和使用,因此把授权对象设置为0.0.0.0
,即允许任何源访问。
至此服务器上的code-server
实例就配置好了,非常方便,然后就可以在任意浏览器中打开{服务器IP}:{8080端口}
访问私有的在线版的VS code
了。
配置好服务器上的code-server实例后,在ipad上也可以用普通浏览器访问,但是浏览器的导航栏和虚拟键盘,还是很影响使用体验。为了解决这个问题,再使用VSApp替代浏览器去连接code-server。
直接在appStore
下载VSApp
打开后选择Self Hosted Server
,填写code-server
的url和自定义密码,服务器的登录信息等
点击保存
VSApp 就可以连接我们自己的 code-server
实例,并且自动登录,由于 VSApp
是一个独立的 iOS 应用,并专门为 code-server
进行了优化,因此体验会比浏览器顺滑得多。
完成以上配置后,打开VSApp
体验一下在线编程,很顺滑,打开VS code
的终端,可以操作服务器上的所有文件,也像pc上一样支持各种extensions
。接下来在服务器上装上git
,node
,nvm
,yarn
等日常环境,在pad上进行一些日常web项目的coding和一些练习就没有问题了,同时也有了在不同终端中一致的开发环境。
16 之前的版本比对更新 VirtualDOM 的过程是采用循环加递归实现的(Stack 算法),这种比对方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件数量庞大,主线程被长期占用,直到整棵 VirtualDOM 树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿,非常影响体验。
核心问题:
在 Fiber 方案中,为了实现任务的终止再继续,DOM比对算法被分成了两部分:
DOM 初始渲染: virtualDOM -> Fiber -> Fiber[] -> DOM
DOM 更新操作: newFiber vs oldFiber -> Fiber[] -> DOM
{
type, // 节点类型 (元素,文本,组件)(具体的类型)
props, // 节点属性
stateNode, // 节点 DOM 对象 | 组件实例对象
tag, // 节点标记(对具体类型的分类 hostRoot || hostComponent || classComponent || functionComponent)
effects, // 数组,存储需要更改的 fiber 对象
effectTag, // 当前Fiber要被执行的操作(新增,删除,修改)
parent, // 当前 Fiber 的父级 Fiber
child, // 当前 Fiber 的子级 Fiber
sibling, // 当前 Fiber 的下一个兄弟 Fiber
alternate // Fiber 备份 fiber比对时使用
}
fiber 树是 Singly Linked List Tree Structure
单链表树结构
相当于“儿子-兄弟表示法”的二叉树 ,child 指向左子节点,sibling 指向右子节点,
首先树上的每个结点是统一的,就是两个指针域,一个叫 FirstChild, 一个叫 NextSibling,第一个指针指向的是它的第一个儿子,右边的指针是指向的下一个兄弟,所有的结点都以这种方式来指向儿子和兄弟,这样就可以把整个树的结点串起来
这种结构是Fiber中使用“循环”来遍历树的关键
最近做了一个商城项目,是在create react app
创建的模版项目上跑eject
命令把内部配置抛出来后魔改的,采用了Mpa+Spa的模式,拆分了多个html入口
和js入口
。
到了要上线的时候,发现在本地生产模式构建竟然需要2个小时,线上容器里跑也要三四十分钟,远远超出了正常的构建时间,影响了项目正常发布。
综合了一下异常的现象,大概有以下几点:
既然是打包的问题,那自然得从webpack
身上找原因了。
结合第1个现象,同时为了方便在本地打包复现,整个过程中我只把打包其中3个页面的过程作为异常样板。
webpack
打包大致有这几个阶段:依赖分析,代码转换,代码优化和压缩。
初步分析配置文件,再结合第2点基本上可以合理推断,问题出在优化和压缩阶段(因为我们的项目中开发和生产模式构建在前两个阶段干的事情基本上差别不大)
为了佐证以上推测,先用webpack-bundle-analyzer
跑了一遍,生产模式下构建产物的体积和依赖关系看起来没有很明显的问题。
先从代码优化和压缩阶段入手,webpack4
这部分基本上都走的optimization
配置,这是这部分的配置代码:
optimization: {
minimize: isEnvProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
sourceMap: shouldUseSourceMap,
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
inline: false,
annotation: true,
}
: false,
},
cssProcessorPluginOptions: {
preset: ["default", { minifyFontValues: { removeQuotes: false } }],
},
}),
],
splitChunks: {
chunks: "all",
name: false,
},
runtimeChunk: true,
},
重点关注optimization
的几个一级配置项。
minimize
是生产模式开启压缩的选项,minimizer
是注册具体进行优化和压缩工作的插件及其具体配置,splitChunks
是配置代码拆分规则,runtimeChunk
是是否单独拆分运行时chunk的选项。
挨个来看,minimize
不用管。
minimizer
这里配置了TerserPlugin
和OptimizeCSSAssetsPlugin
两个插件,分别处理js
和css
,都有嫌疑,先待定。
splitChunks
这里配置的是默认的自动拆分规则,webpack
会自动把公共的依赖拆分到单独的bundle
,其实从之前webpack-bundle-analyzer
的分析结果比较容易看出拆分的包没有什么问题,同时也可以看到runtimeChunk
也正常被拆分出来了, 基本上排除嫌疑了。
回到minimizer
,想不如干,直接拿着这两个插件(只开启其中一个拆件)各跑了一遍build。在禁用OptimizeCSSAssetsPlugin
的时候,构建耗时正常了!
好了,现在问题的范围缩小了,是OptimizeCSSAssetsPlugin
!
再来贴一遍这个插件的配置:
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
inline: false,
annotation: true,
}
: false,
},
cssProcessorPluginOptions: {
preset: ["default", { minifyFontValues: { removeQuotes: false } }],
},
})
先不管它是如何造成问题的,先看看这个插件做了什么事情。
也就两件事:优化css和压缩css。OptimizeCSSAssetsPlugin
通过processor(默认是cssnano
,基于postcss
)优化css代码,再进行压缩。
到这里发挥一下“想象力”,OptimizeCSSAssetsPlugin
是处理的css的,我们项目是使用scss的,也就是说构建到这一步的 时候scss到css已经转换完毕了,我们可以确定是转换后得到的css产物出了问题,这个锅OptimizeCSSAssetsPlugin
不背啊,我只是进一步处理这些问题产物的!
接下来对css构建产物进行检查,对比了正常和异常的css构建产物,文件大小基本是差不多的,内容基本也是差不多的。但是为何耗时却差距几十倍上百倍?
我意识到OptimizeCSSAssetsPlugin
干的一个很重要的事情被我忽略了,那就是去除重复代码!
再发挥一下“想象力”,异常的css产物中有大量重复代码,它们增加了OptimizeCSSAssetsPlugin
的负担,但是被OptimizeCSSAssetsPlugin
去除后看起来却很正常的一样,这样就说得通了。大量的重复代码,这是不正常的!
再次检查了css构建产物,这些代码引起了我的注意:
.m-1{margin:1px!important}
.pt-1{padding-top:1px!important}
.pl-1{padding-left:1px!important}
.pb-1{padding-bottom:1px!important}
.pr-1{padding-right:1px!important}
.p-1{padding:1px!important}
.mt-2{margin-top:2px!important}
.ml-2{margin-left:2px!important}
.mb-2{margin-bottom:2px!important}
/* 此处脑补... */
有经验的同学应该不陌生,这是一些Atom CSS
,很多项目中都会使用到,一般在入口文件中统一引入作为全局样式使用,在我们的项目中是写在一个叫margin-padding.scss
的文件中。
全局搜索这个文件名, 在webpack配置文件
中发现了这些代码:
{
loader: "sass-resources-loader",
options: {
resources: [
path.resolve(__dirname, "../src/styles/variables.scss"),
path.resolve(__dirname, "../src/styles/customTheme.scss"),
path.resolve(
__dirname,
"../src/styles/margin-padding.scss"
),
],
},
}
好家伙,真凶有点呼之欲出了。
记(git)忆(history)告诉我这是之前某同学为了在scss文件中自动注入变量而使用的一个loader。
variables.scss
,customTheme.scss
是一些项目中使用到的顶层scss变量,转换后其实不会产生任何css代码,没什么问题。
这个margin-padding.scss
问题就有点大了,贴一段它的完整代码:
@for $member from 0 through 200 {
.mt-#{$member} {
margin-top: #{$member}px !important;
}
.ml-#{$member} {
margin-left: #{$member}px !important;
}
.mb-#{$member} {
margin-bottom: #{$member}px !important;
}
.mr-#{$member} {
margin-right: #{$member}px !important;
}
.m-#{$member} {
margin: #{$member}px !important;
}
.pt-#{$member} {
padding-top: #{$member}px !important;
}
.pl-#{$member} {
padding-left: #{$member}px !important;
}
.pb-#{$member} {
padding-bottom: #{$member}px !important;
}
.pr-#{$member} {
padding-right: #{$member}px !important;
}
.p-#{$member} {
padding: #{$member}px !important;
}
}
结合这个sass-resources-loader
来看, 好家伙真的好家伙,原来我们给每个scss文件注入了2000条样式,而且它们全部被转换成了真正的css,并且经过合并,最后进入了要交到OptimizeCSSAssetsPlugin
手中处理的css文件中!
其实查阅sass-resources-loader
的文档,文档中已经给出了警告:
Do not include anything that will be actually rendered in CSS, because it will be added to every imported Sass file.
不要包含任何实际将在CSS中呈现的内容,因为它将被添加到每个导入的Sass文件中!
到这里,基本宣告破案了。
事实上,我们已经全局引入了margin-padding.scss
,并且它是可以正常工作的,再通过sass-resources-loader
去注入完全是多余并且错误的,所以去掉这一句就好。
去掉以后再运行全项目打包进行验证,1分钟跑完了,喜闻乐见。
后来偶然间发现了speed-measure-webpack-plugin
这个神器,它可以测量webpack
构建期间各个阶段花费的时间,直接就可以找出瓶颈在哪,重点突破了。
我用这个工具重新跑了一遍之前的异常构建,工具很直观地显示出OptimizeCssAssetsWebpackPlugin
花费了大量的时间。
回过头来看,如果一开始就使用speed-measure-webpack-plugin
分析,可以省去手动定位到OptimizeCssAssetsWebpackPlugin
之前所花费的时间。
在cra项目中的使用也很简单:
// /scripts/build.js
const configFactory = require("../config/webpack.config");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
// Generate configuration
const config = configFactory("production");
// const compiler = webpack(config);
const compiler = webpack(smp.wrap(config));
这个工具不仅可以用来排查异常,也可以对分析和优化webpack构建速度提供很好的帮助。
该配置方案基于约定式,通过glob匹配文件路由,自动根据文件路径和创建web服务器(在本项目中是devServer或者是nginx)路由的映射。
加入一个新的页面时不需要额外修改配置,只需要执行gen命令:
yarn run gen
通过plop工具在src/pages下创建新的页面入口文件夹然后通过start和build命令启动和构建:
yarn start
开发模式启动devServer,自动把入口文件路径路径映射到devServer的路由yarn build
生产模式构建同时生成相应的nginx配置文件,产物可以直接接入项目的CI/CD进行升级scripts
下的构建脚本适配多页webpack
配置文件
这里将MPA入口文件夹统一放置src/pages
目录下,配合glob库动态获取每个入口的html Entry和js Entry的绝对路径, 并且挂载在config/paths.js
导出的路径对象中:
// config/paths.js
// 各页面的对应的模板文件路径
const htmlEntrys = glob.sync(path.join(__dirname, "../src/pages/*/index.html"));
// 各页面的对应的模板文件路径
const jsEntrys = glob.sync(path.join(__dirname, "../src/pages/*/index.tsx"));
module.exports = {
htmlEntrys,
jsEntrys
...
}
进行多页面拆分后,开发环境需要在http server中设置不同入口和路由的映射。
对于我们使用的webpack dev server来说,即配置它的historyApiFallback选项中的rewrites属性。
为了保持自动化,这里我们需要根据动态的入口路径,动态生成rewrites配置。
// config/webpackDevServer.config.js
// 动态生成rewrites配置
const mapHtmlEntry = () => {
let rewrites = [];
// 对于全局根路由,严格匹配并且指向首页入口
// 首页入口没有前端路由分发
rewrites.push({ from: /^\/$/, to: "/index/index.html" });
// 根据入口文件夹名称设置路由映射规则
paths.htmlEntrys.forEach((htmlEntryPath) => {
const entryName = htmlEntryPath.match(/pages\/(.*)\/index.html/)[1];
// 对于其他入口的根路由,注意这里只匹配开头
// 因为其他入口可能有前端路由分发
// 因此只要开头匹配成功,就映射到对应入口html
let regRule = new RegExp(`^\/${entryName}`);
const rule = { from: regRule, to: `/${entryName}/index.html` };
if (entryName !== "index") rewrites.push(rule);
});
// 设置匹配失败时的fallback页面
rewrites.push({ from: /./, to: "/error/index.html" });
return rewrites;
};
module.exports = function (proxy, allowedHost) {
return {
historyApiFallback: {
rewrites: mapHtmlEntry(),
},
...
}
}
用于构建项目的scripts中使用了checkRequiredFiles方法来检查打包入口文件是否存在,在文件缺失时会抛出错误。
这里需要将原本传入的参数由单入口修改为多入口。
// scripts/start.js && scripts/build.js
if (!checkRequiredFiles(paths.htmlEntrys.concat(paths.jsEntrys))) {
process.exit(1);
}
接下来还需要处理webpack配置中多页相关的选项entry、output和插件HtmlWebpackPlugins、ManifestPlugins来完成多页打包。
output可以通过webpack变量设置,而另外三项需要通过我们获取的动态入口路径生成,所以封装了getMpaConfig方法统一处理。
该方法接收一个webpack mode参数,读取各入口的paths,返回与多页入口相对应的entrys对象,HtmlWebpackPlugins和ManifestPlugins实例数组。
// config/getMpaConfig.js
const paths = require("./paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
// Get the Mpa config
// Return Mpa entrys, htmlWebpackPlugins and manifestPlugins
function getMpaConfig(webpackEnv) {
// some code
return {
entrys,
htmlWebpackPlugins,
manifestPlugins,
};
}
module.exports = getMpaConfig;
多页的entrys是对象,每个key表示一个js入口的bundle名,对应的值为js Entry的绝对路径,因此在getMpaConfig中有如下代码获得entrys:
paths.jsEntrys.forEach((jsEntryPath) => {
const entryName = jsEntryPath.match(/pages\/(.*)\/index.tsx/)[1];
entrys[entryName] = [
isEnvDevelopment &&
require.resolve("react-dev-utils/webpackHotDevClient"),
jsEntryPath,
].filter(Boolean);
});
每个HtmlWebpackPlugin只会处理和生成一个html文件,因此我们需要为每一个html配置一个HtmlWebpackPlugin实例。
paths.htmlEntrys.forEach((htmlEntryPath) => {
const entryName = htmlEntryPath.match(/pages\/(.*)\/index.html/)[1];
const htmlWebpackPluginProdConfig = {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
};
htmlWebpackPlugins.push(
new HtmlWebpackPlugin(
Object.assign(
{},
{
filename: `${entryName}/index.html`,
inject: true,
template: htmlEntryPath,
chunks: [entryName],
},
isEnvProduction ? htmlWebpackPluginProdConfig : undefined
)
)
);
});
和Htmlwebpackplugin类似, 需要为每一个打包入口应用一个ManifestPlugins实例生成对应的manifest文件,在getMpaConfig中有如下代码获得ManifestPlugins:
paths.htmlEntrys.forEach((htmlEntryPath) => {
const entryName = htmlEntryPath.match(/pages\/(.*)\/index.html/)[1];
manifestPlugins.push(
new ManifestPlugin({
fileName: `asset-${entryName}-manifest.json`,
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints[entryName].filter(
(fileName) => !fileName.endsWith(".map")
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
})
);
// some code
});
以上准备工作完成后,就可以修改webpack.config.js完成打包配置了。
首先在配置文件导出方法中使用getMpaConfig获得所需要的配置:
module.exports = function (webpackEnv) {
// 获得MPA相关的配置
const { entrys, htmlWebpackPlugins, manifestPlugins } = getMpaConfig(
webpackEnv
);
// some code
}
直接将这几个对象应用在配置中:
module.exports = function (webpackEnv) {
// some code
return {
...,
entry: entrys,
plugins: [
...
].filter(Boolean)
.concat(htmlWebpackPlugins)
.concat(manifestPlugins),
}
}
接下来需要修改output,因为单入口时,output的输出配置是死的,而现在需要根据多入口动态输出:
module.exports = function (webpackEnv) {
// some code
return {
...
output: {
...,
filename: isEnvProduction
? "[name]/[name].[contenthash:8].js"
: isEnvDevelopment && "[name]/[name]-bundle.js",
chunkFilename: isEnvProduction
? "[name]/[name].[contenthash:8].chunk.js"
: isEnvDevelopment && "[name]/[name].chunk.js",
}
}
}
最后还需要修改MiniCssExtractPlugin配置,类似output,它需要根据不同html入口动态输出打包后的css文件。
修改MiniCssExtractPlugin构造函数参数对象中的filename和chunkFilename为[name]/[name].[contenthash:8].css
,[name]
和[contenthash:8]
是webpack变量。
[name]
指当前入口文件的文件名,[contenthash:8]
表示8位hash值,用于防止浏览器缓存。
基于以下一些原则对打包输出的文件结构进行了约束:
.
├── 0
│ ├── 0.9feccc66.chunk.js
│ ├── 0.9feccc66.chunk.js.LICENSE.txt
│ └── 0.9feccc66.chunk.js.map
├── ...
├── 30
│ ├── 9.2bc6a003.chunk.js
│ └── 9.2bc6a003.chunk.js.map
├── account
│ ├── account.18278e5b.chunk.css
│ ├── account.18278e5b.chunk.css.map
│ ├── account.29e3e08d.chunk.js
│ ├── account.29e3e08d.chunk.js.map
│ └── index.html
├── goods
│ ├── goods.18278e5b.chunk.css
│ ├── goods.18278e5b.chunk.css.map
│ ├── goods.38c6746f.chunk.js
│ ├── goods.38c6746f.chunk.js.map
│ └── index.html
├── index
│ ├── index.18278e5b.chunk.css
│ ├── index.18278e5b.chunk.css.map
│ ├── index.95abb4d4.chunk.js
│ ├── index.95abb4d4.chunk.js.map
│ └── index.html
├──usercenter
│ ├── index.html
│ ├── usercenter.0b9899c5.chunk.js
│ ├── usercenter.0b9899c5.chunk.js.map
│ ├── usercenter.18278e5b.chunk.css
│ └── usercenter.18278e5b.chunk.css.map
├── runtime~account
│ ├── runtime~account.811fc362.js
│ └── runtime~account.811fc362.js.map
├── runtime~goods
│ ├── runtime~goods.482bb187.js
│ └── runtime~goods.482bb187.js.map
├── runtime~index
│ ├── runtime~index.965f58d1.js
│ └── runtime~index.965f58d1.js.map
├── runtime~usercenter
│ ├── runtime~usercenter.a1049c67.js
│ └── runtime~usercenter.a1049c67.js.map
├── static
│ └── media
│ ├── login-icon.35d858fb.png
│ ├── logo.ea5bfd44.png
│ ├── no-enable-org.bc7034a1.png
│ └── regist-success.4001541c.png
├── service-worker.js
├── asset-account-manifest.json
├── asset-goods-manifest.json
├── asset-index-manifest.json
├── asset-usercenter-manifest.json
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
├── precache-manifest.5b9d0af9d96a174ce5370a116fdc2cd3.js
└── robots.txt
src/pages
路径下的MPA入口文件夹名Astexplorer.net. 查看ast
visit一个节点的过程: enter -> 遍历子节点(深度优先) -> exit
通过visitor函数对遍历到的ast进行处理,具体操作ast使用path的api,还可以通过state来在遍历过程中传递一些数据
path是遍历过程中的路径,会保留上下文信息,有很多属性和方法,比如:
这些属性和方法是获取当前节点以及它的关联节点的
作用域
信息这个属性可以获取作用域的信息
traverse(ast, {
visitor: {
...
}
})
创建、判断ast节点,提供了xxx、isXxx、assertXxx等api
批量创建ast
ast转换完之后打印成目标代码字符串
当有错误信息要打印的时候,需要打印错误位置的代码
集成之前的包,完成整个编译流程,从源码到目标代码,生成sourcemap
export default function(api, options,dirname) {
return {
inherits: parentPlugin,
manipulateOptions(options, parserOptions) {
options.xxx = '';
},
pre(file) {
this.cache = new Map();
},
visitor: {
StringLiteral(path, state) {
this.xxxx.
}
}
}
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.