Webpack的使用-进阶篇

目录

  • Webpack的使用-进阶篇
    • 一、create-react-app react-project
    • 二、vue create vue-project
    • 三、自定义loader
      • 3.1预备知识
      • 3.2自定义babel-loader
    • 四、自定义plugin
      • 4.1 预备知识-compiler钩子
        • 4.1.2 tapable
        • 4.1.2 compiler钩子
      • 4.2 预备知识-compilation钩子
        • 4.2.1 小插曲:nodejs环境中调试
        • 4.2.2 compilation钩子
      • 4.3 自定义CopyWebpackPlugin
    • 五、自定义Webpack
      • 5.1 Webpack 执行流程
      • 5.2 准备工作
      • 5.3 使用babel解析文件
      • 5.4 模块化
      • 5.5 收集所有的依赖
      • 5.6 生成打包之后的bundle
    • 参考资料

主要包含以下几部分内容:

  • React/Vue脚手架的详细配置
  • 基于Webpack5自定义loader/plugin
  • 自己实现一个简易的Webpack5

在学习本章之前可以先学习Webpack 的使用-基础篇

且可配合源码使用(⊙o⊙)… Webpack 的使用-基础篇源码

基础篇主要讲述如下内容:

  • Webpack 简介
  • Webpack 初体验
  • Webpack 开发环境的基本配置
  • Webpack 生产环境的基本配置
  • Webpack 优化配置
  • Webpack 配置详情
  • Webpack5 使用

更详细的Webpack配置可以查看官网Webpack官网

Webpack 的使用——进阶篇源码

一、create-react-app react-project

本部分只讲述通过脚手架创建的项目的分析路线及步骤,具体每个文件夹里面讲述了什么内容分别在源码中进行注释讲解。

通过 npm run eject将配置文件暴露出来

  1. config–>paths.js(向外暴露出路径)
  2. scripts–>start.js(开发环境对应的文件)
  3. webpack.config.js(主要内容为对loader和plugin的配置,将来自己修改的时候可以直接在这个文件夹里面进行loader和plugin的修改)(核心)
  4. scripts–>build.js(生产环境对应的文件,与开发环境对应的文件差不多)

二、vue create vue-project

这里只讲述通过脚手架创建的项目的分析路线及步骤,具体每个文件夹里面讲述了什么内容分别在源码中进行注释。

  • 通过vue inspect --mode=development > webpack.dev.js将vue开发环境配置打包一起放在webpack.dev.js文件下面,开发环境代码只需要研究webpack.dev.js文件即可
  • 通过vue inspect --mode=production > webpack.prod.js将vue生产环境配置打包一起放在webpack.prod.js文件下面,生产环境代码只需要研究webpack.prod.js文件即可

开发环境文件webpack.dev.js 生产环境文件webpack.prod.js(除了在css上面以及多线程打包上面进行了一些修改,其余和开发环境是一样的)

三、自定义loader

3.1预备知识

loader本质上是一个函数

  1. loader的执行顺序在use数组里面是从下往上执行
  2. loader里面有一个pitch方法,use数组中pitch方法的执行顺序是从上往下执行,因此我们如果想先执行某些功能,可以先在pitch方法中定义
  3. 同步loader
// 方式一
module.exports = function (content, map, meta) {console.log(111);return content;
}
// 方式二
module.exports = function (content, map, meta) {console.log(111);this.callback(null, content, map, meta);
}module.exports.pitch = function () {console.log('pitch 111');
}
  1. 异步loader
// 异步loader(推荐使用,loader在异步加载的过程中可以执行其余的步骤)
module.exports = function (content, map, meta) {console.log(222);const callback = this.async();setTimeout(() => {callback(null, content);}, 1000)
}module.exports.pitch = function () {console.log('pitch 222');
}
  1. 获取options库:

安装loader-utils:cnpm install loader-utils 在loader中引入并使用 6. 校验options库: 在loader中从schema-utils引入validate并使用 创建schema.json文件校验规则并引入使用

loader3.js中代码

// 1.1 获取options 引入
const {getOptions
} = require('loader-utils');
// 2.1 获取validate(校验options是否合法)引入
const {validate
} = require('schema-utils');// 2.3创建schema.json文件校验规则并引入使用
const schema = require('./schema');module.exports = function(content, map, meta) {// 1.2 获取options 使用const options = getOptions(this);console.log(333, options);// 2.2校验options是否合法 使用validate(schema, options, {name: 'loader3'})return content;
}module.exports.pitch = function() {console.log('pitch 333');
}

schema.json中代码

{"type": "object","properties": {"name": {"type": "string","description": "名称~"}},"additionalProperties": false // 如果设置为true表示除了校验前面写的string类型还可以  接着  校验其余类型,如果为false表示校验了string类型之后不可以再校验其余类型
}

webpack.config.js中代码

const path = require('path');module.exports = {module: {rules: [{test: /\.js$/,use: [{loader: 'loader3',// options部分options: {name: 'jack',age: 18}}]}]},// 配置loader解析规则:我们的loader去哪个文件夹下面寻找(这里表示的是同级目录的loaders文件夹下面寻找)resolveLoader: {modules: ['node_modules',path.resolve(__dirname, 'loaders')]}}

3.2自定义babel-loader

  1. 创建校验规则

babelSchema.json

{"type": "object","properties": {"presets": {"type": "array"}},"addtionalProperties": true
}
  1. 创建loader

babelLoader.js

const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');
const babel = require('@babel/core');
const util = require('util');const babelSchema = require('./babelSchema.json');// babel.transform用来编译代码的方法
// 是一个普通异步方法
// util.promisify将普通异步方法转化成基于promise的异步方法
const transform = util.promisify(babel.transform);module.exports = function (content, map, meta) {// 获取loader的options配置const options = getOptions(this) || {};// 校验babel的options的配置validate(babelSchema, options, {name: 'Babel Loader'});// 创建异步const callback = this.async();// 使用babel编译代码transform(content, options).then(({code, map}) => callback(null, code, map, meta)).catch((e) => callback(e))}
  1. babelLoader使用

webpack.config.js

const path = require('path');module.exports = {module: {rules: [{test: /\.js$/,loader: 'babelLoader',options: {presets: ['@babel/preset-env']}}]},// 配置loader解析规则:我们的loader去哪个文件夹下面寻找(这里表示的是同级目录的loaders文件夹下面寻找)resolveLoader: {modules: ['node_modules',path.resolve(__dirname, 'loaders')]}}

四、自定义plugin

4.1 预备知识-compiler钩子

4.1.2 tapable

hooks tapable

  1. 安装tapable:npm install tapable -D
  2. 初始化hooks容器 2.1 同步hooks,任务会依次执行:SyncHook、SyncBailHook 2.2 异步hooks,异步并行:AsyncParallelHook,异步串行:AsyncSeriesHook
  3. 往hooks容器中注册事件/添加回调函数
  4. 触发hooks
  5. 启动文件:node tapable.test.js

文件tapable.test.js

const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require('tapable');class Lesson {constructor() {// 初始化hooks容器this.hooks = {// 同步hooks,任务会依次执行// go: new SyncHook(['address'])// SyncBailHook:一旦有返回值就会退出~go: new SyncBailHook(['address']),// 异步hooks// AsyncParallelHook:异步并行// leave: new AsyncParallelHook(['name', 'age']),// AsyncSeriesHook: 异步串行leave: new AsyncSeriesHook(['name', 'age'])}
}
tap() {// 往hooks容器中注册事件/添加回调函数this.hooks.go.tap('class0318', (address) => {console.log('class0318', address);return 111;})this.hooks.go.tap('class0410', (address) => {console.log('class0410', address);})// tapAsync常用,有回调函数this.hooks.leave.tapAsync('class0510', (name, age, cb) => {setTimeout(() => {console.log('class0510', name, age);cb();}, 2000)})// 需要返回promisethis.hooks.leave.tapPromise('class0610', (name, age) => {return new Promise((resolve) => {setTimeout(() => {console.log('class0610', name, age);resolve();}, 1000)})})
}start() {// 触发hooksthis.hooks.go.call('c318');this.hooks.leave.callAsync('jack', 18, function () {// 代表所有leave容器中的函数触发完了,才触发console.log('end~~~');});
}
}const l = new Lesson();
l.tap();
l.start();

4.1.2 compiler钩子

  1. 工作方式:异步串行执行,因此下面代码输出顺序如下: 1.1 emit.tap 111 1.2 1秒后输出 emit.tapAsync 111 1.3 1秒后输出 emit.tapPromise 111 1.4 afterEmit.tap 111 1.5 done.tap 111
  2. tapAsync和tapPromise表示异步
  3. 这边只简单介绍了几个complier,具体开发的过程中可以根据文档介绍编写(很方便的)
class Plugin1 {apply(complier) {complier.hooks.emit.tap('Plugin1', (compilation) => {console.log('emit.tap 111');})complier.hooks.emit.tapAsync('Plugin1', (compilation, cb) => {setTimeout(() => {console.log('emit.tapAsync 111');cb();}, 1000)})complier.hooks.emit.tapPromise('Plugin1', (compilation) => {return new Promise((resolve) => {setTimeout(() => {console.log('emit.tapPromise 111');resolve();}, 1000)})})complier.hooks.afterEmit.tap('Plugin1', (compilation) => {console.log('afterEmit.tap 111');})complier.hooks.done.tap('Plugin1', (stats) => {console.log('done.tap 111');})}
}module.exports = Plugin1;

4.2 预备知识-compilation钩子

4.2.1 小插曲:nodejs环境中调试

  1. package.json中输入(–inspect-brk 表示通过断点的方式调试,,,,,,./node_modules/webpack/bin/webpack.js" 表示调试这个文件,,,,,,node 表示通过node运行)
"scripts": {"start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"}
  1. 在需要调试的地方打一个debugger
  2. 通过node运行文件
  3. 在一个网站中右击检查,点击绿色图标

便可以调试了,和在网页中调试代码一样的

4.2.2 compilation钩子

  1. 初始化compilation钩子
  2. 往要输出资源中,添加一个a.txt文件
  3. 读取b.txt中的内容,将b.txt中的内容添加到输出资源中的b.txt文件中 3.1 读取b.txt中的内容需要使用node的readFile模块 3.2 将b.txt中的内容添加到输出资源中的b.txt文件中除了使用 2 中的方法外,还有两种形式可以使用 3.2.1 借助RawSource 3.2.2 借助RawSource和emitAsset
const fs = require('fs');
const util = require('util');
const path = require('path');const webpack = require('webpack');
const { RawSource } = webpack.sources;// 将fs.readFile方法变成基于promise风格的异步方法
const readFile = util.promisify(fs.readFile);/*1. 初始化compilation钩子2. 往要输出资源中,添加一个a.txt文件3. 读取b.txt中的内容,将b.txt中的内容添加到输出资源中的b.txt文件中3.1 读取b.txt中的内容需要使用node的readFile模块3.2  将b.txt中的内容添加到输出资源中的b.txt文件中除了使用 2 中的方法外,还有两种形式可以使用3.2.1 借助RawSource3.2.2 借助RawSource和emitAsset
*/class Plugin2 {apply(compiler) {// 1.初始化compilation钩子compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {// debugger// console.log(compilation);// 添加资源compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {// debugger// console.log(compilation);const content = 'hello plugin2';// 2.往要输出资源中,添加一个a.txtcompilation.assets['a.txt'] = {// 文件大小size() {return content.length;},// 文件内容source() {return content;}}const data = await readFile(path.resolve(__dirname, 'b.txt'));// 3.2.1 compilation.assets['b.txt'] = new RawSource(data);// 3.2.1compilation.emitAsset('b.txt', new RawSource(data));cb();})})}}module.exports = Plugin2;

4.3 自定义CopyWebpackPlugin

CopyWebpackPlugin的功能:将public文件夹中的文件复制到dist文件夹下面(忽略index.html文件)

  1. 创建schema.json校验文件
{"type": "object","properties": {"from": {"type": "string"},"to": {"type": "string"},"ignore": {"type": "array"}},"additionalProperties": false
}
  1. 创建CopyWebpackPlugin.js插件文件

编码思路 下载schema-utils和globby:npm install globby schema-utils -D 将from中的资源复制到to中,输出出去 1. 过滤掉ignore的文件 2. 读取paths中所有资源 3. 生成webpack格式的资源 4. 添加compilation中,输出出去

const path = require('path');
const fs = require('fs');
const {promisify} = require('util')const { validate } = require('schema-utils');
const globby = require('globby');// globby用来匹配文件目标
const webpack = require('webpack');const schema = require('./schema.json');
const { Compilation } = require('webpack');const readFile = promisify(fs.readFile);
const {RawSource} = webpack.sourcesclass CopyWebpackPlugin {constructor(options = {}) {// 验证options是否符合规范validate(schema, options, {name: 'CopyWebpackPlugin'})this.options = options;}apply(compiler) {// 初始化compilationcompiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {// 添加资源的hookscompilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {// 将from中的资源复制到to中,输出出去const { from, ignore } = this.options;const to = this.options.to ? this.options.to : '.';// context就是webpack配置// 运行指令的目录const context = compiler.options.context; // process.cwd()// 将输入路径变成绝对路径const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);// 1. 过滤掉ignore的文件// globby(要处理的文件夹,options)const paths = await globby(absoluteFrom, { ignore });console.log(paths); // 所有要加载的文件路径数组// 2. 读取paths中所有资源const files = await Promise.all(paths.map(async (absolutePath) => {// 读取文件const data = await readFile(absolutePath);// basename得到最后的文件名称const relativePath = path.basename(absolutePath);// 和to属性结合// 没有to --> reset.css// 有to --> css/reset.css(对应webpack.config.js中CopyWebpackPlugin插件的to的名称css)const filename = path.join(to, relativePath);return {// 文件数据data,// 文件名称filename}}))// 3. 生成webpack格式的资源const assets = files.map((file) => {const source = new RawSource(file.data);return {source,filename: file.filename}})// 4. 添加compilation中,输出出去assets.forEach((asset) => {compilation.emitAsset(asset.filename, asset.source);})cb();})})}}module.exports = CopyWebpackPlugin;
  1. 在webpack.config.js中使用

五、自定义Webpack

5.1 Webpack 执行流程

  1. 初始化 Compiler:webpack(config) 得到 Compiler 对象
  2. 开始编译:调用 Compiler 对象 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,递归直到所有模块被加载进来
  5. 完成模块编译: 在经过第 4 步使用 Loader 编译完所有模块后,得到了每个模块被编译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。(注意:这步是可以修改输出内容的最后机会)
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

5.2 准备工作

  1. 创建文件夹myWebpack
  2. 创建src–>(add.js / count.js / index.js),写入对应的js代码
  3. 创建config–>webpack.config.js写入webpack基础配置(entry和output)
  4. 创建lib文件夹,里面写webpack的主要配置
  5. 创建script–>build.js(将lib文件夹下面的myWebpack核心代码和config文件下的webpack基础配置引入并调用run()函数开始打包)
  6. 为了方便启动,控制台通过输入命令 npm init -y拉取出package.json文件,修改文件中scripts部分为"build": "node ./script/build.js"表示通过在终端输入命令npm run build时会运行/script/build.js文件,在scripts中添加"debug": "node --inspect-brk ./script/build.js"表示通过在终端输入命令npm run debug时会调试/script/build.js文件中的代码,调试代码的步骤第四章已经介绍

5.3 使用babel解析文件

  1. 创建文件lib–>myWebpack1–>index.js
  2. 下载三个babel包 babel官网

npm install @babel/parser -D用来将代码解析成ast抽象语法树 npm install @babel/traverse -D用来遍历ast抽象语法树代码 npm install @babel/core-D用来将代码中浏览器不能识别的语法进行编译 3. 编码思路 1. 读取入口文件内容 2. 将其解析成ast抽象语法树 3. 收集依赖 4. 编译代码:将代码中浏览器不能识别的语法进行编译

index.js

const fs = require('fs');
const path = require('path');// babel的库
const babelParser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');function myWebpack(config) {return new Compiler(config);
}class Compiler {constructor(options = {}) {this.options = options;}// 启动webpack打包run() {// 1. 读取入口文件内容// 入口文件路径const filePath = this.options.entry;const file = fs.readFileSync(filePath, 'utf-8');// 2. 将其解析成ast抽象语法树const ast = babelParser.parse(file, {sourceType: 'module' // 解析文件的模块化方案是 ES Module})// debugger;console.log(ast);// 获取到文件文件夹路径const dirname = path.dirname(filePath);// 定义存储依赖的容器const deps = {}// 3. 收集依赖traverse(ast, {// 内部会遍历ast中program.body,判断里面语句类型// 如果 type:ImportDeclaration 就会触发当前函数ImportDeclaration({node}) {// 文件相对路径:'./add.js'const relativePath = node.source.value;// 生成基于入口文件的绝对路径const absolutePath = path.resolve(dirname, relativePath);// 添加依赖deps[relativePath] = absolutePath;}})console.log(deps);// 4. 编译代码:将代码中浏览器不能识别的语法进行编译const { code } = transformFromAst(ast, null, {presets: ['@babel/preset-env']})console.log(code);}
}module.exports = myWebpack;

5.4 模块化

我们开发代码过程中讲究的是模块化开发,不同功能的代码放在不同的文件中 创建myWebpack2–>parser.js(放入解析代码)/Compiler.js(放入编译代码)/index.js(主文件)

5.5 收集所有的依赖

所有代码位于myWebpack文件夹中 Compiler.js文件中build函数用于构建代码,run函数中modules通过递归遍历收集所有的依赖,depsGraph用于将依赖整理更好依赖关系图(具体的代码功能都在代码中进行了注释)

5.6 生成打包之后的bundle

代码位于myWebpack–>Compiler.js中的bundle部分 整个myWebpack–>Compiler.js代码

const path = require('path');
const fs = require('fs');
const {getAst,getDeps,getCode
} = require('./parser')class Compiler {constructor(options = {}) {// webpack配置对象this.options = options;// 所有依赖的容器this.modules = [];}// 启动webpack打包run() {// 入口文件路径const filePath = this.options.entry;// 第一次构建,得到入口文件的信息const fileInfo = this.build(filePath);this.modules.push(fileInfo);// 遍历所有的依赖this.modules.forEach((fileInfo) => {/**{'./add.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/add.js','./count.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/count.js'} */// 取出当前文件的所有依赖const deps = fileInfo.deps;// 遍历for (const relativePath in deps) {// 依赖文件的绝对路径const absolutePath = deps[relativePath];// 对依赖文件进行处理const fileInfo = this.build(absolutePath);// 将处理后的结果添加modules中,后面遍历就会遍历它了~(递归遍历)this.modules.push(fileInfo);}})console.log(this.modules);// 将依赖整理更好依赖关系图/*{'index.js': {code: 'xxx',deps: { 'add.js': "xxx" }},'add.js': {code: 'xxx',deps: {}}}*/const depsGraph = this.modules.reduce((graph, module) => {return {...graph,[module.filePath]: {code: module.code,deps: module.deps}}}, {})console.log(depsGraph);this.generate(depsGraph)}// 开始构建build(filePath) {// 1. 将文件解析成astconst ast = getAst(filePath);// 2. 获取ast中所有的依赖const deps = getDeps(ast, filePath);// 3. 将ast解析成codeconst code = getCode(ast);return {// 文件路径filePath,// 当前文件的所有依赖deps,// 当前文件解析后的代码code}}// 生成输出资源generate(depsGraph) {/* index.js的代码"use strict";\n' +'\n' +'var _add = _interopRequireDefault(require("./add.js"));\n' +'\n' +'var _count = _interopRequireDefault(require("./count.js"));\n' +'\n' +'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +'\n' +'console.log((0, _add["default"])(1, 2));\n' +'console.log((0, _count["default"])(3, 1));*/const bundle = `(function (depsGraph) {// require目的:为了加载入口文件function require(module) {// 定义模块内部的require函数function localRequire(relativePath) {// 为了找到要引入模块的绝对路径,通过require加载return require(depsGraph[module].deps[relativePath]);}// 定义暴露对象(将来我们模块要暴露的内容)var exports = {};(function (require, exports, code) {eval(code);})(localRequire, exports, depsGraph[module].code);// 作为require函数的返回值返回出去// 后面的require函数能得到暴露的内容return exports;}// 加载入口文件require('${this.options.entry}');})(${JSON.stringify(depsGraph)})`// 生成输出文件的绝对路径const filePath = path.resolve(this.options.output.path, this.options.output.filename)// 写入文件fs.writeFileSync(filePath, bundle, 'utf-8');}
}module.exports = Compiler;

参考资料

1: 感谢熊健老师的视频讲解!

2:注释很清楚的一篇关于webpack基础的博客

Webpack的使用——进阶篇相关推荐

  1. Enterprise Library Step By Step系列(十二):异常处理应用程序块——进阶篇

    一.把异常信息Logging到数据库 在日志和监测应用程序块中,有朋友提意见说希望能够把异常信息Logging到数据库中,在这里介绍一下具体的实现方法. 1.创建相关的数据库环境: 我们可以用日志和监 ...

  2. Docker 数据卷之进阶篇

    Docker 数据卷之进阶篇 原文:Docker 数据卷之进阶篇 笔者在<Docker 基础 : 数据管理>一文中介绍了 docker 数据卷(volume) 的基本用法.随着使用的深入, ...

  3. C#使用Xamarin开发可移植移动应用进阶篇(7.使用布局渲染器,修改默认布局),附源码...

    原文:C#使用Xamarin开发可移植移动应用进阶篇(7.使用布局渲染器,修改默认布局),附源码 前言 系列目录 C#使用Xamarin开发可移植移动应用目录 源码地址:https://github. ...

  4. Kafka核心设计与实践原理总结:进阶篇

    作者:未完成交响曲,资深Java工程师!目前在某一线互联网公司任职,架构师社区合伙人! kafka作为当前热门的分布式消息队列,具有高性能.持久化.多副本备份.横向扩展能力.我学习了<深入理解K ...

  5. 计算机编程书籍-笨办法学Python 3:基础篇+进阶篇

    编辑推荐: 适读人群 :本书适合所有已经开始使用Python的技术人员,包括初级开发人员和已经升级到Python 3.6版本以上的经验丰富的Python程序员. "笨办法学"系列, ...

  6. 最快让你上手ReactiveCocoa之进阶篇

    前言 由于时间的问题,暂且只更新这么多了,后续还会持续更新本文<最快让你上手ReactiveCocoa之进阶篇>,目前只是简短的介绍了些RAC核心的一些方法,后续还需要加上MVVM+Rea ...

  7. SQL Server调优系列进阶篇(如何维护数据库索引)

    前言 上一篇我们研究了如何利用索引在数据库里面调优,简要的介绍了索引的原理,更重要的分析了如何选择索引以及索引的利弊项,有兴趣的可以点击查看. 本篇延续上一篇的内容,继续分析索引这块,侧重索引项的日常 ...

  8. mysql 开发进阶篇系列 10 锁问题 (使用“索引或间隙锁”的锁冲突)

    1.使用"相同索引键值"的冲突 由于mysql 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但如果是使用相同的索引键,是会出现锁冲突的.设计时要注意 例 ...

  9. Java进阶篇(一)——接口、继承与多态

    前几篇是Java的入门篇,主要是了解一下Java语言的相关知识,从本篇开始是Java的进阶篇,这部分内容可以帮助大家用Java开发一些小型应用程序,或者一些小游戏等等. 本篇的主题是接口.继承与多态, ...

  10. PowerShell攻防进阶篇:nishang工具用法详解

    PowerShell攻防进阶篇:nishang工具用法详解 导语:nishang,PowerShell下并肩Empire,Powersploit的神器. 开始之前,先放出个下载地址! 下载地址:htt ...

最新文章

  1. SAP Customer Data Cloud(Gigya)的用户搜索实现 1
  2. JQuery使用笔记
  3. 每天看一片代码系列(三):codepen上一个音乐播放器的实现
  4. 让我放弃FastDFS拥抱MinIO的8个瞬间
  5. 一个IT时代的终结:109岁的IBM将分拆为两家公司
  6. 微信公众号开发--微信JS-SDK扫一扫功能
  7. 安装electron-react-boilerplate遇到的问题
  8. python中ndarray除_Numpy 基本除法运算和模运算
  9. Java 并发 —— yield/sleep、wait/notify、join
  10. Transformer-XL 2
  11. MT2503环境搭建步骤及注意事项
  12. cAdvisor的使用
  13. 电脑分屏没有声音_电脑用HDMI线分屏后,耳机或音箱没声音之完美解决!
  14. zigbee网关 CC2530 zstack用手机显示终端传来的lm75a温度传感器的值
  15. VPP /什么是VPP?
  16. 多个文本对比相似度分析
  17. CR渲染器全景图如何渲染颜色通道_【3D】日不落投影灯 VR/CR投影效果制作
  18. Win10 This app can't run on this PC Cisco v*pn 0440
  19. 使用ZED相机录制事件双目数据集
  20. C#入门4——计算自由落体运动

热门文章

  1. Android性能优化(二)内存优化
  2. 盘点飞机上的各种警报
  3. 电脑开机出现警报音后提示要按F1才能进入的分析处理
  4. matlab中dcd是什么,dcd是什么意思
  5. 为什么要有虚拟内存?
  6. css中标准盒模型和怪异盒模型的区别,如何将标准盒模型转换为怪异盒模型
  7. Allegro中Change铜皮至其他层
  8. mysql登陆案例_Mysql用户登陆验证过程 案例
  9. Excel如何批量生成条形码?
  10. 在计算机中360云盘如何删除文件,如何在360云盘中检索已删除的文件