深入webpack打包原理,loader和plugin的实现

2023-11-10

本文讨论的核心内容如下:

  1. webpack进行打包的基本原理
  2. 如何自己实现一个loaderplugin

注: 本文使用的webpack版本是v4.43.0, webpack-cli版本是v3.3.11node版本是v12.14.1npm版本v6.13.4(如果你喜欢yarn也是可以的),演示用的chrome浏览器版本81.0.4044.129(正式版本) (64 位)

1. webpack打包基本原理

webpack的一个核心功能就是把我们写的模块化的代码,打包之后,生成可以在浏览器中运行的代码,我们这里也是从简单开始,一步步探索webpack的打包原理

1.1 一个简单的需求

我们首先建立一个空的项目,使用npm init -y快速初始化一个package.json,然后安装webpack webpack-cli

接下来,在根目录下创建src目录,src目录下创建index.jsadd.jsminus.js,根目录下创建index.html,其中index.html引入index.js,在index.js引入add.jsminus.js

目录结构如下:

 

 

 

文件内容如下:

// add.js
export default (a, b) => {
    return a + b
}
// minus.js
export const minus = (a, b) => {
    return a - b
}
// index.js
import add from './add.js'
import { minus } from './minus.js'

const sum = add(1, 2)
const division = minus(2, 1)
console.log('sum>>>>>', sum)
console.log('division>>>>>', division)
复制代码
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>demo</title>
</head>
<body>
    <script src="./src/index.js"></script>
</body>
</html>
复制代码

这样直接在index.html引入index.js的代码,在浏览器中显然是不能运行的,你会看到这样的错误

Uncaught SyntaxError: Cannot use import statement outside a module
复制代码

是的,我们不能在script引入的js文件里,使用es6模块化语法

1.2 实现webpack打包核心功能

我们首先在项目根目录下再建立一个bundle.js,这个文件用来对我们刚刚写的模块化js代码文件进行打包

我们首先来看webpack官网对于其打包流程的描述:

it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle)

在正式开始之前,结合上面webpack官网说明进行分析,明确我们进行打包工作的基本流程如下:

  1. 首先,我们需要读到入口文件里的内容(也就是index.js的内容)
  2. 其次,分析入口文件,递归的去读取模块所依赖的文件内容,生成依赖图
  3. 最后,根据依赖图,生成浏览器能够运行的最终代码

1. 处理单个模块(以入口为例)

1.1 获取模块内容

既然要读取文件内容,我们需要用到node.js的核心模块fs,我们首先来看读到的内容是什么:

// bundle.js
const fs = require('fs')
const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    console.log(body)
}
getModuleInfo('./src/index.js')
复制代码

我们定义了一个方法getModuleInfo,这个方法里我们读出文件内容,打印出来,输出的结果如下图:

 

 

我们可以看到,入口文件index.js的所有内容都以字符串形式输出了,我们接下来可以用正则表达式或者其它一些方法,从中提取到import以及export的内容以及相应的路径文件名,来对入口文件内容进行分析,获取有用的信息。但是如果importexport的内容非常多,这会是一个很麻烦的过程,这里我们借助babel提供的功能,来完成入口文件的分析

 

1.2 分析模块内容

我们安装@babel/parser,演示时安装的版本号为^7.9.6

这个babel模块的作用,就是把我们js文件的代码内容,转换成js对象的形式,这种形式的js对象,称做抽象语法树(Abstract Syntax Tree, 以下简称AST)

// bundle.js
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
        // 表示我们要解析的是es6模块
       sourceType: 'module' 
    })
    console.log(ast)
    console.log(ast.program.body)
}
getModuleInfo('./src/index.js')
复制代码

使用@babel/parserparse方法把入口文件转化称为了AST,我们打印出了ast,注意文件内容是在ast.program.body中,如下图所示:

 

 

入口文件内容被放到一个数组中,总共有六个Node节点,我们可以看到,每个节点有一个type属性,其中前两个的type属性是ImportDeclaration,这对应了我们入口文件的两条import语句,并且,每一个type属性是ImportDeclaration的节点,其source.value属性是引入这个模块的相对路径,这样我们就得到了入口文件中对打包有用的重要信息了。

 

接下来要对得到的ast做处理,返回一份结构化的数据,方便后续使用。

1.3 对模块内容做处理

ast.program.body部分数据的获取和处理,本质上就是对这个数组的遍历,在循环中做数据处理,这里同样引入一个babel的模块@babel/traverse来完成这项工作。

安装@babel/traverse,演示时安装的版本号为^7.9.6

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
       sourceType: 'module' 
    })
    const deps = {}
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(file);
            const absPath = './' + path.join(dirname, node.source.value)
            deps[node.source.value] = absPath
        }
    })
    console.log(deps)
}
getModuleInfo('./src/index.js')
复制代码

创建一个对象deps,用来收集模块自身引入的依赖,使用traverse遍历ast,我们只需要对ImportDeclaration的节点做处理,注意我们做的处理实际上就是把相对路径转化为绝对路径,这里我使用的是Mac系统,如果是windows系统,注意斜杠的区别

获取依赖之后,我们需要对ast做语法转换,把es6的语法转化为es5的语法,使用babel核心模块@babel/core以及@babel/preset-env完成

安装@babel/core @babel/preset-env,演示时安装的版本号均为^7.9.6

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
       sourceType: 'module' 
    })
    const deps = {}
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(file);
            const absPath = './' + path.join(dirname, node.source.value)
            deps[node.source.value] = absPath
        }
    })
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    const moduleInfo = { file, deps, code }
    console.log(moduleInfo)
    return moduleInfo
}
getModuleInfo('./src/index.js')
复制代码

如下图所示,我们最终把一个模块的代码,转化为一个对象形式的信息,这个对象包含文件的绝对路径,文件所依赖模块的信息,以及模块内部经过babel转化后的代码

 

 

2. 递归的获取所有模块的信息

这个过程,也就是获取依赖图(dependency graph)的过程,这个过程就是从入口模块开始,对每个模块以及模块的依赖模块都调用getModuleInfo方法就行分析,最终返回一个包含所有模块信息的对象

const parseModules = file => {
    // 定义依赖图
    const depsGraph = {}
    // 首先获取入口的信息
    const entry = getModuleInfo(file)
    const temp = [entry]
    for (let i = 0; i < temp.length; i++) {
        const item = temp[i]
        const deps = item.deps
        if (deps) {
            // 遍历模块的依赖,递归获取模块信息
            for (const key in deps) {
                if (deps.hasOwnProperty(key)) {
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    temp.forEach(moduleInfo => {
        depsGraph[moduleInfo.file] = {
            deps: moduleInfo.deps,
            code: moduleInfo.code
        }
    })
    console.log(depsGraph)
    return depsGraph
}
parseModules('./src/index.js')
复制代码

获得的depsGraph对象如下图:

 

 

 

我们最终得到的模块分析数据如上图所示,接下来,我们就要根据这里获得的模块分析数据,来生产最终浏览器运行的代码。

3. 生成最终代码

在我们实现之前,观察上一节最终得到的依赖图,可以看到,最终的code里包含exports以及require这样的语法,所以,我们在生成最终代码时,要对exports和require做一定的实现和处理

我们首先调用之前说的parseModules方法,获得整个应用的依赖图对象:

const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
}
复制代码

接下来我们应该把依赖图对象中的内容,转换成能够执行的代码,以字符串形式输出。 我们把整个代码放在自执行函数中,参数是依赖图对象

const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
        function require(file) {
            var exports = {};
            return exports
        }
        require('${file}')
    })(${depsGraph})`
}
复制代码

接下来内容其实很简单,就是我们取得入口文件的code信息,去执行它就好了,使用eval函数执行,初步写出代码如下:

const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
        function require(file) {
            var exports = {};
            (function(code){
                eval(code)
            })(graph[file].code)
            return exports
        }
        require('${file}')
    })(${depsGraph})`
}
复制代码

上面的写法是有问题的,我们需要对file做绝对路径转化,否则graph[file].code是获取不到的,定义adsRequire方法做相对路径转化为绝对路径

const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
        function require(file) {
            var exports = {};
            function absRequire(relPath){
                return require(graph[file].deps[relPath])
            }
            (function(require, exports, code){
                eval(code)
            })(absRequire, exports, graph[file].code)
            return exports
        }
        require('${file}')
    })(${depsGraph})`
}
复制代码

接下来,我们只需要执行bundle方法,然后把生成的内容写入一个JavaScript文件即可

const content = bundle('./src/index.js')
// 写入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)
复制代码

最后,我们在index.html引入这个./dist/bundle.js文件,我们可以看到控制台正确输出了我们想要的结果

 

 

4. bundle.js的完整代码

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    console.log(body)
    const ast = parser.parse(body, {
       sourceType: 'module' 
    })
    // console.log(ast.program.body)
    const deps = {}
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(file);
            const absPath = './' + path.join(dirname, node.source.value)
            deps[node.source.value] = absPath
        }
    })
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    const moduleInfo = { file, deps, code }
    return moduleInfo
}

const parseModules = file => {
    // 定义依赖图
    const depsGraph = {}
    // 首先获取入口的信息
    const entry = getModuleInfo(file)
    const temp = [entry]
    for (let i = 0; i < temp.length; i++) {
        const item = temp[i]
        const deps = item.deps
        if (deps) {
            // 遍历模块的依赖,递归获取模块信息
            for (const key in deps) {
                if (deps.hasOwnProperty(key)) {
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    temp.forEach(moduleInfo => {
        depsGraph[moduleInfo.file] = {
            deps: moduleInfo.deps,
            code: moduleInfo.code
        }
    })
    // console.log(depsGraph)
    return depsGraph
}


// 生成最终可以在浏览器运行的代码
const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
        function require(file) {
            var exports = {};
            function absRequire(relPath){
                return require(graph[file].deps[relPath])
            }
            (function(require, exports, code){
                eval(code)
            })(absRequire, exports, graph[file].code)
            return exports
        }
        require('${file}')
    })(${depsGraph})`
}


const build = file => {
    const content = bundle(file)
    // 写入到dist/bundle.js
    fs.mkdirSync('./dist')
    fs.writeFileSync('./dist/bundle.js', content)
}

build('./src/index.js')
复制代码

2. 手写loaderplugin

2.1 如何自己实现一个loader

loader本质上就是一个函数,这个函数会在我们在我们加载一些文件时执行

2.1.1 如何实现一个同步loader

首先我们初始化一个项目,项目结构如图所示:

 

其中index.js和webpack.config.js的文件内容如下:

// index.js
console.log('我要学好前端,因为学好前端可以: ')

// webpack.config.js
const path = require('path')
module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    }
}
复制代码

我们在根目录下创建syncLoader.js,用来实现一个同步的loader,注意这个函数必须返回一个buffer或者string

// syncloader.ja
module.exports = function (source) {
    console.log('source>>>>', source)
    return source
}
复制代码

同时,我们在webpack.config.js中使用这个loader,我们这里使用resolveLoader配置项,指定loader查找文件路径,这样我们使用loader时候可以直接指定loader的名字

const path = require('path')
module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    resolveLoader: {
        // loader路径查找顺序从左往右
        modules: ['node_modules', './']
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: 'syncLoader'
            }
        ]
    }
}
复制代码

接下来我们运行打包命令,可以看到命令行输出了source内容,也就是loader作用文件的内容。

 

接着我们改造我们的loader:

 

module.exports = function (source) {
    source += '升值加薪'
    return source
}
复制代码

我们再次运行打包命令,去观察打包后的代码:

 

这样,我们就实现了一个简单的loader,为我们的文件增加一条信息。 我们可以尝试在loader的函数里打印this,发现输出结果是非常长的一串内容,this上有很多我们可以在loader中使用的有用信息,所以,对于loader的编写,一定不要使用箭头函数,那样会改变this的指向。

 

一般来说,我们会去使用官方推荐的loader-utils包去完成更加复杂的loader的编写

我们继续安装loader-utils,版本是^2.0.0

我们首先改造webpack.config.js

const path = require('path')

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    resolveLoader: {
        // loader路径查找顺序从左往右
        modules: ['node_modules', './']
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'syncLoader',
                    options: {
                        message: '升值加薪'
                    }
                }
            }
        ]
    }
}
复制代码

注意到,我们为我们的loader增加了options配置项,接下来在loader函数里使用loader-utils获取配置项内容,拼接内容,我们依然可以得到与之前一样的打包结果

// syncLoader.js
const loaderUtils = require('loader-utils')
module.exports = function (source) {
    const options = loaderUtils.getOptions(this)
    console.log(options)
    source += options.message
    // 可以传递更详细的信息
    this.callback(null, source)
}
复制代码

 

 

 

 

这样,我们就完成了一个简单的同步loader的编写

2.1.2 如何实现一个异步loader

和同步loader的编写方式非常相似,我们在根目录下建立一个asyncLoader.js的文件,内容如下:

const loaderUtils = require('loader-utils')
module.exports = function (source) {
    const options = loaderUtils.getOptions(this)
    const asyncfunc = this.async()
    setTimeout(() => {
        source += '走上人生颠覆'
        asyncfunc(null, res)
    }, 200)
}
复制代码

注意这里的this.async(),用官方的话来说就是Tells the loader-runner that the loader intends to call back asynchronously. Returns this.callback.也就是让webpack知道这个loader是异步运行,返回的是和同步使用时一致的this.callback

接下来我们修改webpack.config.js

const path = require('path')
module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    resolveLoader: {
        // loader路径查找顺序从左往右
        modules: ['node_modules', './']
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: 'syncLoader',
                        options: {
                            message: '走上人生巅峰'
                        }
                    },
                    {
                        loader: 'asyncLoader'
                    }
                ]
            }
        ]
    }
}
复制代码

注意loader执行顺序是从下网上的,所以首先为文本写入‘升值加薪’,然后写入‘走上人生巅峰’

 

到此,我们简单介绍了如何手写一个loader,在实际项目中,可以考虑一部分公共的简单逻辑,可以通过编写一个loader来完成(比如国际化文本替换)

2.2 如何自己实现一个plugin

plugin通常是在webpack在打包的某个时间节点做一些操作,我们使用plugin的时候,一般都是new Plugin()这种形式使用,所以,首先应该明确的是,plugin应该是一个类。

我们初始化一个与上一接实现loader时候一样的项目,根目录下创建一个demo-webpack-plugin.js的文件,我们首先在webpack.config.js中使用它

const path = require('path')
const DemoWebpackPlugin = require('./demo-webpack-plugin')
module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    plugins: [
        new DemoWebpackPlugin()
    ]
}
复制代码

再来看demo-webpack-plugin.js的实现

class DemoWebpackPlugin {
    constructor () {
        console.log('plugin init')
    }
    apply (compiler) {

    }
}

module.exports = DemoWebpackPlugin
复制代码

我们在DemoWebpackPlugin的构造函数打印一条信息,当我们执行打包命令时,这条信息就会输出,plugin类里面需要实现一个apply方法,webpack打包时候,会调用pluginaplly方法来执行plugin的逻辑,这个方法接受一个compiler作为参数,这个compilerwebpack实例

plugin的核心在于,apply方法执行时,可以操作webpack本次打包的各个时间节点(hooks,也就是生命周期勾子),在不同的时间节点做一些操作

关于webpack编译过程的各个生命周期勾子,可以参考Compiler Hooks

同样,这些hooks也有同步和异步之分,下面演示compiler hooks的写法,一些重点内容可以参考注释:

class DemoWebpackPlugin {
    constructor () {
        console.log('plugin init')
    }
    // compiler是webpack实例
    apply (compiler) {
        // 一个新的编译(compilation)创建之后(同步)
        // compilation代表每一次执行打包,独立的编译
        compiler.hooks.compile.tap('DemoWebpackPlugin', compilation => {
            console.log(compilation)
        })
        // 生成资源到 output 目录之前(异步)
        compiler.hooks.emit.tapAsync('DemoWebpackPlugin', (compilation, fn) => {
            console.log(compilation)
            compilation.assets['index.md'] = {
                // 文件内容
                source: function () {
                    return 'this is a demo for plugin'
                },
                // 文件尺寸
                size: function () {
                    return 25
                }
            }
            fn()
        })
    }
}

module.exports = DemoWebpackPlugin
复制代码

我们的这个plugin的作用就是,打包时候自动生成一个md文档,文档内容是很简单的一句话

上述异步hooks的写法也可以是以下两种:

// 第二种写法(promise)
compiler.hooks.emit.tapPromise('DemoWebpackPlugin', (compilation) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve()
        }, 1000)
    }).then(() => {
        console.log(compilation.assets)
        compilation.assets['index.md'] = {
            // 文件内容
            source: function () {
                return 'this is a demo for plugin'
            },
            // 文件尺寸
            size: function () {
                return 25
            }
        }
    })
})
// 第三种写法(async await)
compiler.hooks.emit.tapPromise('DemoWebpackPlugin', async (compilation) => {
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve()
        }, 1000)
    })
    console.log(compilation.assets)
    compilation.assets['index.md'] = {
        // 文件内容
        source: function () {
            return 'this is a demo for plugin'
        },
        // 文件尺寸
        size: function () {
            return 25
        }
    }
})
复制代码

最终的输出结果都是一样的,在每次打包时候生成一个md文档

 

到此为止,本文介绍了webpack打包的基本原理,以及自己实现loader和plugin的方法。希望本文内容能对大家对webpack的学习,使用带来帮助,谢谢大家。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入webpack打包原理,loader和plugin的实现 的相关文章

随机推荐

  • jmeter压测步骤

    参考 使用Jmeter压测的第一个接口 第一步 在测试计划里添加一个线程组 要压测的接口名称 如图所示 在测试计划里右键 添加 线程 线程组就可以了 第二步 设置线程组参数 如下图所示 第三步 添加请求 在线程组上右键 添加 取样器 HTT
  • 服务器virsh不显示虚机,【openEuler 20.09】【虚拟化】virsh attach-interface热插rtl8139网卡后,虚拟机内部不显示,重启后才显示...

    环境信息 Host NAME openEuler VERSION 20 09 ID openEuler VERSION ID 20 09 PRETTY NAME openEuler 20 09 ANSI COLOR 0 31 Guest C
  • Python函数(def, return)

    函数 函数 Function 喂 给函数一些数据 它就能内部消化 给你 吐 出你想要的东西 这就像自动贩卖机 只不过贩卖机是喂点钱 吐出来一些吃的喝的用的东西 而Python函数则是喂各种各样的数据 吐出来各种各样的功能 函数定义 在Pyt
  • c#对字符串的各种操作

    1 字符串定义 2 在字符串后面追加字符串 3 获取字符串长度 4 截取字符串的一部分 5 字符串转为比特码 6 查指定位置是否为空字符 7 查字符串是否是标点符号 8 截头去尾 Trim 9 替换字符串 10 得到用单个字符串分隔字符串单
  • MT7688路由器 openwrt编译笔记

    Openwrt 19 07 4 路由器平台 MT7688 代码下载 git clone git git openwrt org openwrt git 或者更快的下载如下 git clone https gitee com mirrors
  • Java实现数据脱敏

    一 什么是数据脱敏 数据脱敏指的是某些敏感的信息通过脱敏规则进行数据的变形 实现敏感隐私数据的可靠保护 敏感数据包括 姓名 身份证号 手机号 银行卡号等信息 防止这些敏感数据在不安全的情况下使用 所以就要使用数据脱敏的技术 使用数据脱敏会在
  • 微服务,那些你该懂的知识(服务的注册和发现)

    微服务 微服务按照我个人的理解就是将众多的功能拆分成一个个子服务 其中以现在很流行的SpringBoot框架进行开发 再以SpringCloud方式进行部署 进而可以在SpringCloud的服务平台中对SpringBoot的一个个服务进行
  • 【算法】——归并排序的解析

    目录 1 归并排序的思想 2 归并排序的分析 3 内排序和外排序 1 归并排序的思想 归并是将两个或两个以上的有序表组合成一个新的有序表 假设初始序列含有n个记录 则可看成是n个子序列 每个子序列的长度为1 然后两两归并 得到 n 2 个长
  • cocos2d中的anchorPoint

    cocos2d中的anchorPoint 将该图片放置到屏幕左下方 CCSprite sprite CCSprite sprite Default png addChild sprite 生成的精灵放置在 0 0 也就是屏幕左下角 但是精灵
  • (springmvc)页面找不到静态资源文件Failed to load resource: the server responded with a status of 404 (Not Found)

    今天打算整理之前写的一个插件功能上传到github 新建一个java项目 项目使用的是spring mvc框架 然后再调试页面的时候发现找不到静态资源文件 如下图所示 于是我第一时间检查jsp上页面资源的路径 经过确认 发现路径是没有
  • line-height(行高)

    line height 行高 line height 行高 介绍 字体框 line height 行高 介绍 1 行高指的是文字占有的实际高度 2 通过line height来设置行高 3 行高可以直接指定一个大小 px em 4 也可以直
  • Unity WebGL三维地球

    1 支持arcgis 天地图 bingmap 谷歌地图 高德地图等影像加载 2 支持高程三维地形加载 3 支持在线 离线数据加载 4 支持unity坐标和经纬度坐标互相转换 5 支持fbx模型放置在地球上 6 支持倾斜摄影数据放置在地球上
  • C#从数据库中读取二进制流并生成文件

    下面以图片文件为例加以说明 从数据库表 图片存储 中读取ID为1的图片数据并生成图片文件 MySqlConnection conn new MySqlConnection Server localhost Database test cha
  • fff

    http www migucloud com vi0 109 3j KJ59CLFb6F9pvcJ1egcF cld450p FILENAME 54 cld450p mp4 duration 201 owner 109 path 109 3
  • linux 杀死进程失败,linux - Ubuntu关闭失败“ *杀死所有剩余进程…” - Ubuntu问答...

    问题描述 我已经重新安装了Ubuntu Server reboot 有效 但是在 Killing all remaining processes 步骤上关闭失败 我在用 sudo shutdown now 在失败之后 由 fail 指示 f
  • 【廖雪峰python入门笔记】函数

    1 函数 我们知道圆的面积计算公式为 S r 当我们知道半径r的值时 就可以根据公式计算出面积 假设我们需要计算3个不同大小的圆的面积 r1 12 34 r2 9 08 r3 73 1 s1 3 14 r1 r1 s2 3 14 r2 r2
  • 深入学习java源码之ArrayList.iterator()与ArrayList.listIterator()

    深入学习java源码之ArrayList iterator 与ArrayList listIterator 内部类的使用典型的情况是 内部类继承自某个类或实现某个接口 内部类的代码操作创建其的外层类的对象 所以你可以认为内部类提供了某种进入
  • php excel导入

    excel导入导出是我们做项目中经常用到的功能 那么 今天就来说说excel导入 一 类文件 二 调用代码
  • 使用 htmx 构建交互式 Web 应用

    学习目标 了解htmx的基本概念 特点和用法 并能够运用htmx来创建交互式的Web应用程序 学习内容 1 什么是htmx htmx是一种用于构建交互式Web应用程序的JavaScript库 它通过将HTML扩展为一种声明性的交互式语言 使
  • 深入webpack打包原理,loader和plugin的实现

    本文讨论的核心内容如下 webpack进行打包的基本原理 如何自己实现一个loader和plugin 注 本文使用的webpack版本是v4 43 0 webpack cli版本是v3 3 11 node版本是v12 14 1 npm版本v