基于postCss的TaiWindCss源码解析

  • 前言
  • 了解 postCss
    • 什么 是 postCss?
    • postCss的核心原理/工作流
  • TaiWindCss 源码解析
    • TaiWindCss 是什么?
    • 实现基本原理
    • 源码文件解读
      • package.json
      • 读取相关配置文件
      • 主要入口处理逻辑
      • 核心代码
        • base 核心代码
        • utilities 核心代码
        • components 核心代码
      • 插件配置
    • 脚手架
  • 总结
  • 参考资料

前言

从最初看到TaiWindCss这个库的时候,单纯从css上看,这块跟自前接触到的bootstrap差不多,就是纯粹的css 样式库而已,但是深入了解之后,发现这个比bootstrap使用的更加方便,也很细,唯一感觉麻烦就是,每次的样式都需要自己去DIV组装,包括一系列hover,active等效果。后来深入查看了解TaiWindCss,发现这css 样式库,竟然是用js代码写出来,然后编译出来的CSS文件样式库。当时就把我给震惊了,js还能这样的操作。有点厉害!!!于是我这边就去细细的去了解并查看TaiWindCss源码。去深究这块的实现逻辑。

了解 postCss

什么 是 postCss?

通俗的讲法:postCss 就是一个开发工具,是一个用 JavaScript 工具和插件转换 CSS 代码的工具。支持变量,混入,未来 CSS 语法,内联图像等等。

它具备以下特性与常见的功能:

  1. 增强代码的可读性:Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。
  2. 将未来的 CSS 特性带到今天!:帮你将最新的 CSS 语法转换成大多数浏览器都能理解的语法,并根据你的目标浏览器或运行时环境来确定你需要的 polyfills
  3. 终结全局 CSS:CSS 模块 能让你你永远不用担心命名太大众化而造成冲突,只要用最有意义的名字就行了。
  4. 避免 CSS 代码中的错误:通过使用 stylelint 强化一致性约束并避免样式表中的错误。stylelint 是一个现代化 CSS 代码检查工具。它支持最新的 CSS 语法,也包括类似 CSS 的语法,例如 SCSS
  5. 可以作为预处理器使用,类似:Sass, Less 和 Stylus。但是 PostCSS 是模块化的工具,比之前那些快3-30 倍,而且功能更强大。并演化出了一系列的插件来使用。

postCss的核心原理/工作流

PostCSS 包括 CSS 解析器,CSS 节点树 API,一个源映射生成器和一个节点树 stringifier。

PostCSS 主要的原理核心工作流:

  1. 通过 fs 读取CSS文件
  2. 通过 parser 将CSS解析成抽象语法树(AST树)
  3. 将AST树”传递”给任意数量的插件处理
  4. 诸多插件进行数据处理。插件间传递的数据就是AST树
  5. 通过 stringifier 将处理完毕的AST树重新转换成字符串

这一系列的工作流,讲简单一点就是对数据进行一系列的操作。为此PostCSS提供了一系列的数据操作API。比如:walkAtRules ,walkComments,walkDecls,walkRules等相关API, 具体相关文档可查看:
postcss官方API文档

TaiWindCss 源码解析

TaiWindCss 是什么?

从TaiWindCss时间的使用场景上来看,我们以 PostCSS 插件的形式安装TaiWindCss,而本质上讲TaiWindCss是一个postCss的插件,我们目前在实际项目开发的角度上看,我们目前都已经普遍使用PostCSS,目前大环境下的大多数的流行/主流的框架基本都默认使用了 PostCSS,例如 autoprefixer. 这个我们就基本上都会使用。

对于PostCSS的插件使用,我们再使用的过程中一般都需要如下步骤:

  1. PostCSS 配置文件 postcss.config.js,新增 tailwindcss 插件。
  2. TaiWindCss插件需要一份配置文件,比如:tailwind.config.js。
  3. 项目 引入的 less,sass,css 文件中注入 @tailwind 标识,并引入 base,components,utilities,是否全部引入取决你自己。

相关配置完成之后,在项目打包或者热更新的过程中,执行 PostCSS一系列插件, 自动把我们页面上引用的相关css,进行打包成我们需要的css文件,然后加载到我们的页面中。

因为我们使用了碎片化的样式文件布局,比如页面上的代码

class="col-start-1 row-start-1 flex sm:col-start-2 sm:row-span-3"

这些 class 类型,高度颗粒化,高度复用,而 TailwindCss 在构建生产文件时会自动删除所有未使用的 CSS,这意味着您最后的 CSS 文件可能会是最小的。

实现基本原理

首先我们明确清楚postCss的工作流:

大致步骤:

  • 将CSS解析成抽象语法树(AST树)
  • 将AST树”传递”给任意数量的插件处理
  • 将处理完毕的AST树重新转换成字符串

在PostCSS中有几个关键的处理机制:

Source string → Tokenizer → Parser → AST → Processor → Stringifier

那么TaiWindCss也是遵循这样的类似的一个工作流:

基本的步骤:

  • 将CSS解析成抽象语法树(AST树)
  • 读取插件配置,根据配置文件,生成新的抽象语法树
  • 将AST树”传递”给一系列数据转换操作处理(变量数据循环生成,切套类名循环等)
  • 清除一系列操作留下的数据痕迹
  • 将处理完毕的AST树重新转换成字符串

举例说明:

当前代码块如下:
@layer components{@variants responsive{.container{width: 100%}}
}
转换后的AST代码块如下:
{"raws": {"semicolon": false,"after": "\n\n"},"type": "root","nodes": [{"raws": {"before": "","between": "","afterName": " ","semicolon": false,"after": "\n"},"type": "atrule","name": "layer","source": {"start": {"line": 1,"column": 1},"input": {"css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n","hasBOM": false,"id": "<input css 17>"},"end": {"line": 7,"column": 1}},"params": "components","nodes": [{"raws": {"before": "\n  ","between": "","afterName": " ","semicolon": false,"after": "\n  "},"type": "atrule","name": "variants","source": {"start": {"line": 2,"column": 3},"input": {"css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n","hasBOM": false,"id": "<input css 17>"},"end": {"line": 6,"column": 3}},"params": "responsive","nodes": [{"raws": {"before": "\n    ","between": "","semicolon": false,"after": "\n    "},"type": "rule","nodes": [{"raws": {"before": "\n      ","between": ": "},"type": "decl","source": {"start": {"line": 4,"column": 7},"input": {"css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n","hasBOM": false,"id": "<input css 17>"},"end": {"line": 4,"column": 17}},"prop": "width","value": "100%"}],"source": {"start": {"line": 3,"column": 5},"input": {"css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n","hasBOM": false,"id": "<input css 17>"},"end": {"line": 5,"column": 5}},"selector": ".container"}]}]}],"source": {"input": {"css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n","hasBOM": false,"id": "<input css 17>"},"start": {"line": 1,"column": 1}}
}

树形结构图:
反复的数据操作,新增删除添加,都是对root -> nodes -> atrule / rule / Comment / Container / Declaration -> nodes 数组,往里面新增数据。这一系列的操作用的postCss文档提供的一系列方法。

如果我们想Css文件变成如下代码:

@layer components{@variants responsive{.container{width: 100%}.c {color: red}}
}

那么我们只需把下面的数据:

{"raws":{"before":"\n    ","between":" ","semicolon":false,"after":"\n    "},"type":"rule","nodes":[{"raws":{"before":"\n    \t","between":": "},"type":"decl","source":{"start":{"line":7,"column":6},"input":{"css":"@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n    .c {\n    \tcolor: red\n    }\n      \n  }\n}\n\n","hasBOM":false,"id":"<input css 37>"},"end":{"line":7,"column":15}},"prop":"color","value":"red"}],"source":{"start":{"line":6,"column":5},"input":{"css":"@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n    .c {\n    \tcolor: red\n    }\n      \n  }\n}\n\n","hasBOM":false,"id":"<input css 37>"},"end":{"line":8,"column":5}},"selector":".c"
}

插入到 root -> nodes -> atrule / rule / Comment / Container / Declaration -> nodes 下面的数据即可。
得到新的树形图如下:
图中,新增的目标就是我们上次出入数据之后,反馈出来的AST树形图机构。

从上述内容,我们基本上就了解TaiWindCss的实现基本原理了。其实就是一个对数据流的一系列操作过程,得到最终我们想要的 CSS 模块,然后再剔除掉多余的代码,转换成我们想要的CSS文件。

源码文件解读

TaiWindCss的github地址: TaiWindCss github 地址

package.json

首先查看package.json文件,我们找到scripts,代码如下:

"scripts": {"prebabelify": "rimraf lib","babelify": "babel src --out-dir lib --copy-files","rebuild-fixtures": "npm run babelify && babel-node scripts/rebuildFixtures.js","prepublishOnly": "npm run babelify && babel-node scripts/build.js","style": "eslint .","test": "jest","posttest": "npm run style","compat": "node scripts/compat.js --prepare","compat:restore": "node scripts/compat.js --restore"},

我们 查看prepublishOnly 这一行,就是TaiWindCss源码构建的入口点。

找到项目文件 scripts/build.js,核心代码如下:

import tailwind from '..'
function buildDistFile(filename, config = {}, outFilename = filename) {return new Promise((resolve, reject) => {fs.readFile(`./${filename}.css`, (err, css) => {if (err) throw errreturn postcss([tailwind(config), require('autoprefixer')]).process(css, {from: `./${filename}.css`,to: `./dist/${outFilename}.css`,}).then((result) => {fs.writeFileSync(`./dist/${outFilename}.css`, result.css)return result}).then((result) => {const minified = new CleanCSS().minify(result.css)fs.writeFileSync(`./dist/${outFilename}.min.css`, minified.styles)}).then(resolve).catch((error) => {console.log(error)reject()})})})
}

这个核心代码描述就是传递文件名(base),读取项目中的css文件,经过postcss插件tailwindcss进行转换的css文件。然后到处Css文件。

值得注意的是

import tailwind from '..'

这里的tailwind,其实就是package.json里面的对应的

 "main": "lib/index.js",

读取相关配置文件

文件目录 src -> index.js
核心代码如下:

const plugin = postcss.plugin('tailwindcss', (config) => {const plugins = []const resolvedConfigPath = resolveConfigPath(config)if (!_.isUndefined(resolvedConfigPath)) {plugins.push(registerConfigAsDependency(resolvedConfigPath))}console.log('plugins:', plugins)return postcss([...plugins,processTailwindFeatures(getConfigFunction(resolvedConfigPath || config)),formatCSS,])
})

定义tailwindcss命名的postCss插件,去解析css文件。
页面上引用的:

import getAllConfigs from './util/getAllConfigs'
import { defaultConfigFile } from './constants'
import defaultConfig from '../stubs/defaultConfig.stub.js'

就是项目初始化的一系列配置文件。其中核心的读取配置文件就是 tailwind.config.js , 而这个就是我们再使用tailwindcss的时候,需要去对我们的引用进行配置化管理的文件。

读取配置的文件,主要是对AST的数据结构进行初始化与替换操作。这个文件再后续的逻辑处理中都是作为参数的形式调用的。

主要入口处理逻辑

src 文件下面的 processTailwindFeatures.js,这个文件是所有操作业务逻辑的入口文件,还有因为PostCss版本不同的引用,核心的入口都是这个文件,一系列逻辑操作代码如下:

return postcss([substituteTailwindAtRules(config, getProcessedPlugins()),evaluateTailwindFunctions(config),substituteVariantsAtRules(config, getProcessedPlugins()),substituteResponsiveAtRules(config),convertLayerAtRulesToControlComments(config),substituteScreenAtRules(config),substituteClassApplyAtRules(config, getProcessedPlugins, configChanged),applyImportantConfiguration(config),purgeUnusedStyles(config, configChanged),]).process(css, { from: _.get(css, 'source.input.file') })
  1. substituteTailwindAtRules 转换AST数据操作
  2. evaluateTailwindFunctions 主题配置操作
  3. substituteVariantsAtRules 变量递归规则操作
  4. substituteResponsiveAtRules 常规Responsive规则逻辑操作
  5. convertLayerAtRulesToControlComments 内容编辑描述操作
  6. substituteScreenAtRules 样式 Screen 规则操作
  7. substituteClassApplyAtRules 标识 @apply 逻辑处理
  8. applyImportantConfiguration 传参 important 是否添加逻辑处理
  9. purgeUnusedStyles 删除多余的代码,添加purgecss插件,读取配置删除多余的未引用的css样式代码
    10.最好导出我们项目开发所需的Css文件

核心代码

src 文件下面的 processTailwindFeatures.js,中有这样一行代码:

processedPlugins = processPlugins([...corePlugins(config), ..._.get(config, 'plugins', [])],config
)getProcessedPlugins = function () {return {// ...jumpUrl,base: cloneNodes(processedPlugins.base),components: cloneNodes(processedPlugins.components),utilities: cloneNodes(processedPlugins.utilities),}
}

其中核心的功能就是遍历src/plugins下面的 一些列配置文件:

'preflight','container','space','divideWidth','divideColor','divideStyle','divideOpacity','accessibility','appearance','backgroundAttachment','backgroundClip','backgroundColor','backgroundImage','gradientColorStops','backgroundOpacity','backgroundPosition','backgroundRepeat','backgroundSize','borderCollapse','borderColor','borderOpacity','borderRadius','borderStyle','borderWidth','boxSizing','cursor','display','flexDirection','flexWrap','placeItems','placeContent','placeSelf','alignItems','alignContent','alignSelf','justifyItems','justifyContent',.....

遍历这些配置生成代码就是util -> processPlugins.js 里面的代码:

handler({postcss,config: getConfigValue,theme: (path, defaultValue) => {const [pathRoot, ...subPaths] = _.toPath(path)const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)return transformThemeValue(pathRoot)(value)},corePlugins: (path) => {if (Array.isArray(config.corePlugins)) {return config.corePlugins.includes(path)}return getConfigValue(`corePlugins.${path}`, true)},variants: (path, defaultValue) => {if (Array.isArray(config.variants)) {return config.variants}return getConfigValue(`variants.${path}`, defaultValue)},e: escapeClassName,prefix: applyConfiguredPrefix,addUtilities: (utilities, options) => {const defaultOptions = { variants: [], respectPrefix: true, respectImportant: true }options = Array.isArray(options)? Object.assign({}, defaultOptions, { variants: options }): _.defaults(options, defaultOptions)const styles = postcss.root({ nodes: parseStyles(utilities) })styles.walkRules((rule) => {if (options.respectPrefix && !isKeyframeRule(rule)) {rule.selector = applyConfiguredPrefix(rule.selector)}if (options.respectImportant && config.important) {rule.__tailwind = {...rule.__tailwind,important: config.important,}}})pluginUtilities.push(wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'utilities'))},addComponents: (components, options) => {const defaultOptions = { variants: [], respectPrefix: true }options = Array.isArray(options)? Object.assign({}, defaultOptions, { variants: options }): _.defaults(options, defaultOptions)const styles = postcss.root({ nodes: parseStyles(components) })styles.walkRules((rule) => {if (options.respectPrefix && !isKeyframeRule(rule)) {rule.selector = applyConfiguredPrefix(rule.selector)}})pluginComponents.push(wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'components'))},addBase: (baseStyles) => {pluginBaseStyles.push(wrapWithLayer(parseStyles(baseStyles), 'base'))},addVariant: (name, generator, options = {}) => {pluginVariantGenerators[name] = generateVariantFunction(generator, options)},})})

循环一些列根据配置所需要的元素,进行遍历逻辑操作,从而生成生成base,components ,utilities 文件。为数据逻辑操作生成原始数据。

base 核心代码

base的核心操作代码如下:

export default function () {return function ({ addBase }) {const normalizeStyles = postcss.parse(fs.readFileSync(require.resolve('modern-normalize'), 'utf8'))const preflightStyles = postcss.parse(fs.readFileSync(`${__dirname}/css/preflight.css`, 'utf8'))addBase([...normalizeStyles.nodes, ...preflightStyles.nodes])}
}

base就是一些基础样式配置,这里面引用了 modern-normalize 这个基础样式库来作为 TaiWindCss 的基础样式库,我们也可以自定义引用,比如代码中的 preflightStyles.css 文件,如果后续需要扩展 TaiWindCss的base基础库,写法类似上传代码中preflight.css代码引用即可。

utilities 核心代码

utilities 的核心操作

(1)我们已经明确css是如何的,比如我们明确字体样式向左,向右,居中等,那么代码如下:

export default function () {return function ({ addUtilities, variants }) {addUtilities({'.text-left': { 'text-align': 'left' },'.text-center': { 'text-align': 'center' },'.text-right': { 'text-align': 'right' },'.text-justify': { 'text-align': 'justify' },},variants('textAlign'))}
}

(2) 我们需要根据配置文件去生成的样式文件,比如z-index 后面的值是配置化的,那么代码如下:

import createUtilityPlugin from '../util/createUtilityPlugin'export default function () {return createUtilityPlugin('zIndex', [['z', ['zIndex']]])
}
components 核心代码

components 这个模板,我的理解,就是提供一系列类似组件化的代码逻辑,TaiWindCss 目前它只对 布局样式 container 做了这样的操作。主要代码如下:

onst atRules = _(minWidths).sortBy((minWidth) => parseInt(minWidth)).sortedUniq().map((minWidth) => {return {[`@media (min-width: ${minWidth})`]: {'.container': {'max-width': minWidth,...generatePaddingFor(minWidth),},},}}).value()addComponents([{'.container': Object.assign({ width: '100%' },theme('container.center', false) ? { marginRight: 'auto', marginLeft: 'auto' } : {},generatePaddingFor(0)),},...atRules,],variants('container')
)
}

从代码逻辑上看,读取配置 screens 的值,遍历生成不同 screens 下面的 container的样式数据。然后进行归类并输出components这个样式库。当然我们也可以对这块进行扩展,比如:

addComponents({'.btn-blue': {backgroundColor: 'blue',color: 'white',padding: '.5rem 1rem',borderRadius: '.25rem',},'.btn-blue:hover': {backgroundColor: 'darkblue',},
})
//或者
addComponents({'.btn-blue': {backgroundColor: 'blue',},},['responsive', 'hover']
)

如果新增这样的插件配置,那么这些板块就是打包到components这个库里面。

这三个模板的配置化生产CSS文件,就是TaiWindCss的主要功能

插件配置

插件的功能, 给我们开发了一个入口函数,让我们可以进行配置化开发,插件的方法如下:

module.exports = {plugins: [plugin(function({ addUtilities, addComponents, e, prefix, config }) {// Add your custom styles here}),]
}

传递的一系列参数,就是util -> processPlugins.js 里面的一些核心代码应用。
主要的核心代码:
入口文件src -> index.js 读取插件配置:

processTailwindFeatures(getConfigFunction(resolvedConfigPath || config)),}

getConfigFunction方法,读取tailwind.config.js配置里面的插件,进行数据初始化操作,然后把这些插件写入进来,从原理上js下写的方法就是类似 plugins 文件夹下的哪些插件js文件。只是调用的方式不同而已。

具体的插件使用文档查看官网 TaiWindCss插件使用文档

脚手架

我们再使用TaiWindCss过程中,需要提供一个npm包,于是TaiWindCss内部也集成了一个TaiWindCss的使用手册(脚手架)。
查看package.json :

 "bin": {"tailwind": "lib/cli.js","tailwindcss": "lib/cli.js"},

根据目录我们知道了,它主要提供三个 init (初始化),build(构建生成css ),help (帮助文档)

核心的代码在 src -> cli -> commands -> init.js help.js,build.js

具体代码很简单,就是简单的脚手架配置js, 这里就不写了,大家有兴趣可以去看源码。

总结

通过这段时间的对TaiWindCss源码的解析,阅读源码,从中学到了代码的一些好的写法,系统架构的设计思想与理念。主要还是有以下的学习成果:

  • AST结构与转换原理
  • 从0到1熟悉postCss,具体文档查看postCss官网
  • postCss 插件的开发流程
  • TaiWindCss框架的设计理念
  • JS插件化/可配置化架构的设计与实现

通过这段时间的阅读与相关代码查看,让我真正熟悉并可以熟练使用postCss这个工具库,还有我觉得很重要的收获就是代码的系统设计。

以前我们的代码设计可能都是正向的,从if else 一直下去,该调用就调用,该传参就传参,设计理念都是至上而下的。

而阅读TaiWindCss的源码之后,让我觉得很受用的就是通过遍历plugins下面的一系列文件,函数传参中传递函数方法,从何获取数据,最终得到原始数据。而这样的设计就是这个架构最核心的设计理念。

这样的反向操作逻辑,代码归类很棒,收集数据通过一个循环就搞定了。函数调用传递参数,每个不同的插件集按需去使用。

这样的设计给与我通过阅读源码来获取到。从而让我感受到了阅读源码带来了一下好处,可以丰富并增长我们的开发实践与开发经验。

后续我会提供一个mini TaiWindCss的开发实践。

参考资料

  • tailwindcss官网
  • tailwindcss Github地址
  • tailwindcss官方博客
  • postCss官网
  • postcss中文官网
  • postcss Github地址
  • astexplorer在线转换
  • purgecss官网
  • 开发一个psotcss插件
  • PostCSS 到底是一个什么东西?

基于postCss的TaiWindCss源码解析相关推荐

  1. 跟着小马哥学系列之 Spring AOP(基于 XML 定义 Advice 源码解析)

    学好路更宽,钱多少加班. --小马哥 简介 大家好,我是小马哥成千上万粉丝中的一员!2019年8月有幸在叩丁狼教育举办的猿圈活动中知道有这么一位大咖,从此结下了不解之缘!此系列在多次学习极客时间< ...

  2. .Net Core 认证系统之基于Identity Server4 Token的JwtToken认证源码解析

    介绍JwtToken认证之前,必须要掌握.Net Core认证系统的核心原理,如果你还不了解,请参考.Net Core 认证组件源码解析,且必须对jwt有基本的了解,如果不知道,请百度.最重要的是你还 ...

  3. Android Hawk数据库的源码解析,Github开源项目,基于SharedPreferences的的存储框架

    今天看了朋友一个项目用到了Hawk,然后写了这边文章 一.了解一下概念 Android Hawk数据库github开源项目 Hawk是一个非常便捷的数据库.操作数据库只需一行代码,能存任何数据类型. ...

  4. Android Hawk的源码解析,一款基于SharedPreferences的存储框架

    转载请标注:http://blog.csdn.net/friendlychen/article/details/76218033 一.概念 SharedPreferences的使用大家应该非常熟悉啦. ...

  5. 基于8.0源码解析:startService 启动过程

    基于8.0源码解析:startService 启动过程 首先看一张startService的图,心里有个大概的预估,跟Activity启动流程比,Service的启动稍微简单点,并且我把Service ...

  6. Spring源码深度解析(郝佳)-学习-源码解析-基于注解切面解析(一)

    我们知道,使用面积对象编程(OOP) 有一些弊端,当需要为多个不具有继承关系的对象引入同一个公共的行为时,例如日志,安全检测等,我们只有在每个对象引用公共的行为,这样程序中能产生大量的重复代码,程序就 ...

  7. Spring源码深度解析(郝佳)-学习-源码解析-基于注解注入(二)

    在Spring源码深度解析(郝佳)-学习-源码解析-基于注解bean解析(一)博客中,己经对有注解的类进行了解析,得到了BeanDefinition,但是我们看到属性并没有封装到BeanDefinit ...

  8. ConcurrentHashMap源码解析——基于JDK1.8

    ConcurrentHashMap源码解析--基于JDK1.8 前言 这篇博客不知道写了多久,总之就是很久,头都炸了.最开始阅读源码时确实是一脸茫然,找不到下手的地方,真是太难了.下面的都是我自己阅读 ...

  9. Mybatis 源码解析 -- 基于配置的源码解析(二)

    为什么80%的码农都做不了架构师?>>>    mapper解析 接着上篇的配置,本篇主要讲解mappers标签 <?xml version="1.0" e ...

最新文章

  1. linux kvm dhcp配置,《转》QEMU-KVM创建虚拟机自动指定IP的配置
  2. Node.js从零开发Web Server博客项目笔记
  3. 焦作的计算机三级考试考点,3月河南计算机等级考试考点分布情况
  4. 主线程等待一个 无阻塞函数 死循环子线程的安全退出
  5. 家庭问题(信息学奥赛一本通-T1362)
  6. 在JSP页面中,对同名的CHECKBOX的处理
  7. 最新pvz服务器补偿码,阴阳师:补偿来了!大量活动导致服务器崩溃,现已修复且下发补偿...
  8. 关于oracle数据库的导出导出
  9. 简单的springBoot集成jedis
  10. 《数据结构题集(C语言版)》第2章(线性表)习题自解答
  11. ChinaITLab-Linux工程师培训课程笔记2
  12. 第十章:如何制定项目目标?
  13. 前后端处理实时刷新refresh_token的使用
  14. 模块九:mouse、key、joystick模块
  15. charAt()-‘0‘
  16. “全民来答题”用户协议
  17. 微信分享点击回到原APP却仍然留在微信的问题
  18. 前端如何处理后端一次性传来的10w条数据
  19. Photoshop合并多个图片为PDF格式文件的(PDF文件编辑删除页面及合并的操作方法)解决方案
  20. 铁血丹心 歌词粤语转汉语谐音

热门文章

  1. 知识表示学习【知识图谱专栏】
  2. IntelliJ IDEA 编码过程中没有错误提示以及自动提示等等的解决方法
  3. R语言绘图:实用脑科学数据可视化包
  4. Acer(宏碁)笔记本Windows10一些热键(大小写锁、数字锁、触控板)开关的悬浮提示
  5. ImageIO读取图片出现一层红色背景解决方案
  6. android Accessibility系统自带语音助手打开第三方应用
  7. cocos creator | 用摄像机实现残影幻影拖尾效果
  8. Centos 8 vim显示行号
  9. 高校圆桌派-第四期话题征集火热开启
  10. lintcode(636)132 Pattern