Webpack——从基础使用到手动实现(万字长文)

发表于 4年以前  | 总阅读数:557 次

写到前面:故虽然我事前也查阅了许许多多的博客专栏书籍,但是由于很多教程都是几年以前的,其不少插件或loader均进行了升级迭代不再是以前的用法,其新的使用方法均需一个一个手动查询。如有误,望指教。感恩!

写在开头 我想这些你应该事先知道

1. webpack是干什么的,它如今在前端是一个什么样的地位

这个其实不用多说了,如今的前端项目哪里还有用不到打包工具的呢,而webpack又作为打包工具中的王者我们又有什么理由不去搞明白它呢。

2. commonjs与ES6 Module不应该了解吗

先简单说一下吧

通常node中便是使用commonjs,其导入使用require,导出使用module.exports

ES6 Module中即导入使用import,导出则有两种

  • 命名导出export
  • 默认导出export default,对外输出了一个名为default的变量,因此不需要像命名导出一样进行变量声明,直接导出值即可。一个文件只能有一个

注意注意的是:在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;而在ES6 Module中则是值的动态映射,并且这个映射是只读的。

还有一些AMD和UMD,模块的东西先了解到这里吧。毕竟这里是为webpack做铺垫的

3. entry&chunk&bundle之间的关系

这张图已经很明确了吧

4 webpack中的核心概念

entry

用于指定本次webpack打包的地址(相对地址即可)如:

单入口
entry:'./src/index.js'
或:
entry:{
    main:'./src/index.js'
}

多入口
entry:{
     main:'./src/index.js',
     other:'./src/other.js'
}

output

用于指定打包完成之后的输出文件地址及文件名,文件地址使用绝对地址

单文件
output:{
    filename:'bundle.js',
    path:path.join(__dirname,'dist')
}

多文件
output:{
    filename:'[name].js',
    path:path.join(__dirname,'dist')
}

mode

用于指定当前构建环境

主要有以下三种选择

  • production
  • development
  • none

设置mode会自动触发webpack的一些内置函数,不写默认为none

loaders

webpack默认是能识别.json、.js、模块,其他模块我们需要借助loaders帮助我们将它们放进依赖图里面

它本质就是一个函数,接收源文件为参数,返回转换后的结果

plugins

plugin可以在webpack运行到某个阶段的时候(webpack利用tapable搞了许多生命周期的构造,方便我们在合适的时间利用插件帮我们做些合适的事情),做一些我们需要的事情。如clean-webpack-plugin会在我们进打包的时候先删除dist下的原输出文件。

一:基础使用

1.1 处理html、css、js

使用webpack-dev-server

我们也是希望自己打包后文件可以在一个本地服务器上启动,webpack-dev-server就是一个这种东西。

安装:npm i webpack-dev-server -D

「新配置webpack.config.js一个devServer属性」

如下:

devServer: {
  port: 3000,
  contentBase: './dist'  // 起服务的地址(即定位到我们的输出文件地址)
  open: true              // 自动打开浏览器
  compress:true         // gzip压缩
}

「为简化命令在package.json中添加一个dev命令」

同时也可以在配一个打包命令,不过我一般喜欢用npx

{
    "name": "webpack-test02",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "build":"webpack",
        "dev": "webpack-dev-server",
    },
 }

使用html-webpack-plugin

可能上面你发现了,没有一个html文件。即使服务器启动也没有什么卵用,接下来我们可以在文件中写一个html模板(只生成骨架即可)然后利用html-webpack-plugin将这个模板同时打包到dist目录下,并引入输出的bundl.js

安装:npm i html-webpack-plugin -D

使用:

const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',//模板文件地址
            filename: 'index.html',//指定打包后的文件名字
            hash: true,//也可给其生成一个hash值

        }),

    ],

    还有以下选项
minify: { // 压缩打包后的html文件
      removeAttributeQuotes: true, // 删除属性双引号
      collapseWhitespace: true // 折叠空行变成一行
    }

处理css

基本处理

这里需要安装两个loader,即css-loader(用于处理css中的@import这种语法)、style-loader用于将css插入head标签

安装:npm i css-loader style-loader -D

使用:

   module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader','css-loader']
            },

        ]
    }

或指定参数,下面是每次将css插入到head标签的前面。以保证我们后面自己在模板中的设置可覆盖
    module: {
        rules: [{
                test: /\.css$/,
                use: [{
                        loader: 'style-loader',
                        options: {
                            insert: function insertAtTop(element) {
                                var parent = document.querySelector('head');
                                // eslint-disable-next-line no-underscore-dangle
                                var lastInsertedElement =
                                    window._lastElementInsertedByStyleLoader;

                                if (!lastInsertedElement) {
                                    parent.insertBefore(element, parent.firstChild);
                                } else if (lastInsertedElement.nextSibling) {
                                    parent.insertBefore(element, lastInsertedElement.nextSibling);
                                } else {
                                    parent.appendChild(element);
                                }

                                // eslint-disable-next-line no-underscore-dangle
                                window._lastElementInsertedByStyleLoader = element;
                            },
                        }
                    },

                    'css-loader'
                ]
            },
                ]}
抽离css

使用插件 mini-css-extract-plugin

安装:npm i mini-css-extract-plugin -D

抽离成了单文件即不需要在用style-loader

使用:

const MinniCssExtractPlugin = require('mini-css-extract-plugin')

    plugins: [
        new HtmlWebpackPlugin({
            // 指定模板文件
            template: './src/index.html',
            // 指定输出的文件名
            filename: 'index.html',
            // 添加hash解决缓存

        }),
        new MinniCssExtractPlugin({
            //指定输出的文件名
            filename: 'main.css'
        })
    ],

      module: {
        rules: [{
                test: /\.css$/,
                use: [
                    MinniCssExtractPlugin.loader,//本质就是创建一个style标签再将输出的css地址引入
                    'css-loader',                
                ]
            },

        ]
    }
压缩css、js

将上面输出的css文件做压缩处理

使用插件optimize-css-assets-webpack-plugin但是使用了此插件压缩css之后,js需要使用uglifyjs-webpack-plugin继续压缩

安装:npm i optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D

使用:(注意此插件不再在plugin中使用了,使用位置为优化optimization属性里面)

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
    optimization: {
        minimizer: [
            new UglifyJsPlugin({
                cache: true,
                parallel: true,
                sourceMap: true
            }),
            new OptimizeCSSAssetsPlugin({})
        ]
    },
添加厂商前缀

需要一个loader和一个样式工具:postcss-loader autoprefixer

「先来说一下什么是postCss」

它是一个编译插件的容器,它的工作模式为接收源代码交由编译插件处理,最后输出css。

postcss-loader就是postCss和webpack的连接器。postcss-loader可以和css-loader一起使用也可以单独使用。注意单独使用postcss-loader的使用css中不建议使用@import语法,否则会产生冗余代码。

postCss还还需要一个单独的配置文件postcss.config.js

下面还看用post-loader和autoprefixer 来生成厂商前缀

安装:npm i postcss-loader autoprefixer -D

使用:webpack.config.js,module的rules中

     {
                test: /\.css$/,
                use: [
                    MinniCssExtractPlugin.loader,
                    'css-loader',
                    'postcss-loader',
                ]
            },

postcss.config.js中

module.exports = {
    plugins: [
        require("autoprefixer")({
            overrideBrowserslist: ["last 2 versions", ">1%"]
        })
    ]
};
处理less

需要使用 less-loader,同时less-loader里面利用的是less故需安装less less-loader

安装:npm i less-loader less -D

使用:

{
                test: /\.less$/,
                use: [
                    MinniCssExtractPlugin.loader,
                    'css-loader',
                    'postcss-loader',
                    'less-loader'
                ]
            }

sass同理

处理js

es6转es5

安装:npm i babel-loader @babel/core @babel/preset-env -D

使用:

{
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            '@babel/preset-env'
                        ],

                    }
                }
            },
es7 转class语法

安装:npm i @babel/plugin-proposal-class-properties -D

使用:

{
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            '@babel/preset-env'
                        ],
                         plugins:[
                            ["@babel/plugin-proposal-class-properties", { "loose" : true }]
            ]

                    }
                }
            },

其他详细见babel官网

1.2 处理图片

一张图片的引用有三种方式

  • js中引入 创建图片标签引入
  • css引入 url
  • html中引入 img

使用file-loader

第三张情况不可用,需要额外使用loader看下面栗子吧

安装:npm i file-loader -D

本质就是引用是返回的是一张图片的地址,不过此张图片是已经在bundle中了

使用:「js中」

import './index.css'
import img from '../public/img/img01.jpg'
const image = new Image()
image.src = img
document.body.appendChild(image)

「css中」

div{
    p{
        width: 100px;
        height: 100px;
        transform: rotate(45deg);
      background-image: url('./img01.jpg');
    }
}

「webpack.config.js中」

module.export={
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: 'file-loader'
      }
    ]
  }
}

使用 url-loader

可当做file-loader的升级版,我们可以设置一张图片小于多少时使用base64,或者使用file-loader打包原图片

安装:npm i url-loader -D

同时这里可以使用html-withimg-loader处理html中的图片了,注意要把esModule属性设置为false。否则链如html中图片地址不对上面同理

安装:npm i html-withimg-loader -D

使用:

{
                test: /\.html$/,
                use: 'html-withimg-loader'
            }, {
                test: /\.(png|jpg|gif)$/,
                use: {
                    loader: 'file-loader',
                    options: {
                        limit: 50 * 1024,
                        loader: 'file-loader',
                        esModule: false,
                        outputPath: '/img/', // 打包后输出地址
                        // publicPath: ''           // 给资源加上域名路径
                    }
                }
            },

1.3 eslint

1.4 常用小插件

cleanWebpackPlugin

每次打包自动删除输出目录下的文件

安装:npm i clean-webpack-plugin -D

使用:

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins:[
      new CleanWebpackPlugin(),

]

copyWebpackPlugin

可以一些没有依赖到,但是又需要输出倒dist下的文件

安装:npm i copy-webpack-plugin -D

const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins:[
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin({
            patterns: [{
                from: path.join(__dirname, 'public'),
                to: 'dist'
            }],
        }),
]

bannerPlugin

代码首部添加版权

webpack上的一个自带插件

使用:

const webpack = require('webpack')
plugins:[
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin({
            patterns: [{
                from: path.join(__dirname, 'public'),
                to: 'dist'
            }],
        }),
      new webpack.BannerPlugin('core by gxb'),
]

二:进价用法

2.1 多页面打包

即多入口,每个入口对应一个生成依赖树

配置很简单,配一下入口。入口格式为一个chunkNmae:path

entry: {
        index: './src/index.js',
        other: './src/other.js'
    },
 output: {
        filename: '[name].js',
        path: path.join(__dirname, './dist')
    },

2.2 devtool :'source-map'

即源代码与打包后的代码之间的映射,就是在代码发生问题时可以通过source-map定位到原代码块

主要是在开发环境下使用

配置如下

devtool: 'source-map'

devtool属性值有许多种,下面总结几个最常用的

  • source-map 会产生映射.map文件,同时也能定位到行列
  • cheap-module-source-map,不会产生.map文件,可定位到行【推荐配置】
  • eval-source-map,不会产生.map文件,可定位行列

注意对于css、less、scss来说要想能定位到源码,还需在loader选项中进行配置

如:(请注意:笔者偷懒此段代码没有进行测试不知道现在过时没有)

test:\/.css$\,
use:[
    'style-loader',
    {
        loader:'css-loader',
        options:{
            sourceMap:true
        }
    }
]

2.3 watch

webpack中也可以配置watch监听器进行时时打包,即我们每次修改完代码之后不需要在自己输入命令了,直接c+s键保存即可。

配置如下:

<module.exports = {
    watch: true,
    watchOptions: {
      poll: 1000, // 每秒询问多少次
      aggregateTimeout: 500,  //防抖 多少毫秒后再次触发防止重复按键
      ignored: /node_modules/ //忽略时时监听
    }
}

2.4 resolve

我们都清楚webpack启动之后会从入口文件开始寻找所有的依赖,但是像寻找一些第三方包的时候它总是会默认去找这个文件main.js为入口文件,但是像bootstrap我们有时候仅仅是需要引用它的样式。如果将它全部拿来打包,那么是不是有点太浪费了呢

resole在这里便可以指定webpack如何寻找模块对象的文件

配置如下:

resolve:{
    modules:[path.join('node_modules')],//指定寻找目录
    mainFields: ['style', 'main'],//优先用style没有则使用main
    extensions: ['.js', '.css', '.json']//如果导入的文件没有后缀名,那么先看js文件有无,再看css...
    alias:{
        components: './src/components/'//增加别名,即使用import Button from 'components/button 导入时,实际上被 alias 等价替换成了  import Button from './src/components/button' 。
    }
}

其他选项请参考此文章

2.5 环境拆分

开发和线上的环境所需要配置的东西一般是不相同的,故可以利用webpack-merge,将配置文件拆分成一个基础公共、一个开发、一个线上的。

我们以后打包时就可以指定指定配置文件用于开发环境下打包或者产品环境下打包了。

安装:npm i webpack-merge -D

写法如下:

基础

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
    entry: './src/index.js',

    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: 'index.html'
        }),
        new CleanWebpackPlugin(),

    ],
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, './dist')
    },

}

开发:webpack.dev.js

const merge = require('webpack-merge')
const common = require('./webpack.config')

module.exports = merge(base, {
  mode: 'development',
  devServer: {},
  devtool: 'source-map'
})

线上:webpack.prod.js

const merge = require('webpack-merge');
 const common = require('./webpack.config.js');

 module.exports = merge(common, {
     mode: 'production',
 });

2.6 处理跨域

笔者前面的文章总结过跨域,即利用代理处理跨域的思想就是:同源策略仅是存在于浏览器,服务器之间是不存在的。故我们可以先把数据发到一个代理服务器上

栗子:像一个不同域服务器发送请求

const xhr = new XMLHttpRequest();
xhr.open('get', '/api/user', true);
xhr.send();

xhr.onload = function () {
  console.log(xhr.response)
}

服务器代码:

const express = require('express')
const app = express()
app.get('/test', (req, res) => {
    res.json({ msg: 11 })
})
app.listen(3001, function() {
    console.log('启动服务');

})

webpack.config.js配置代理

    devServer: {
        port: 3000,
        proxy: {
            '/api': {
                target: 'http://localhost:3001',
                pathRewrite: { '^/api': '' }
            }
        },
        progress: true,
        contentBase: './dist',
        open: true
    },

三:优化

3.1 noparse

引用一些第三方包的时候,我们是不需要再进入这些包中再去寻找依赖,因为一般情况下均是独立的。

故不必再去解析浪费时间了

配置如下:

module: {
    noParse: /jquery/, // 不用解析某些包的依赖
        rules:[]
}

3.2 include&exclude

同时我们也可以指定寻找的范围

如:

rules: [
  {
    test: /\.js$/,
    exclude: '/node_modules/',   // 排除node_modules
    include: path.resolve('src'),  // 在src文件寻找
    use: {
      loader: 'babel-loader',
      options: {
        presets: [
          '@babel/preset-env',
        ]
      }
    }
  }

不建议使用exclude,要使用绝对路径

3.3 lgnorePlugin插件

有些第三方库中许多东西是我们用不到的,如一些有语言包的库。我们一般情况下是仅需要使用它里面的中文包的,其他各种语言包我们是不需要的。

故这是可以使webpack中提供的一个插件

const webpack = require('webpack')

plugins: [
    new webpack.IgnorePlugin(/\.\/locale/, /moment/)
]

3.4 多线程打包

使用插件happypack

安装:npm i happypack

使用:

const Happypack = require('happypack')

module:{
    rules: [{
            test: /\.css$/,
            use: 'happypack/loader?id=css'
        }]
}
plugins:[
     new Happypack({
            id: 'css',
            use: ['style-loader', 'css-loader']
        })
]

3.5 懒加载(按需加载)

即有些东西我们不用马上把它导入我们的依赖树中,而是用到它之后再导进来(就类似于按需加载)

这里需要@babel/plugin-syntax-dynamic-import用于 解析识别import()动态导入语法

安装:npm i @babel/plugin-syntax-dynamic-import -D

栗子:

index.js中:

const button = document.createElement('button')
button.innerHTML = '按钮'
button.addEventListener('click', () => {
    console.log('click')
    import ('./source.js').then(data => {
        console.log(data.default)

    })

})
document.body.appendChild(button)

source.js

export default 'gxb'

webpack.config.js

{
  test: /\.js$/,
  include: path.resolve('src'),
  use: [{
    loader: 'babel-loader',
    options: {
      presets: [
        '@babel/preset-env',
      ],
      plugins: [
        '@babel/plugin-syntax-dynamic-import'
      ]
    }
  }]
}

source.js模块仅仅当我们点击了按钮之后才会加入依赖,否则不会立即打包

3.6 热更新

即代码更新是页面只会更新该更新的地方而不是重新渲染整个页面,即重新刷新页面

热更新插件也是webpack上自带的

配置如下:

devServer: {
        hot: true,
        port: 3000,
        contentBase: './dist',
        open: true
    },
 plugins:[
     new webpack.HotModuleReplacementPlugin()
 ]

3.7 DllPlugin动态链接库

我们使用vue或react时,每次打包文件均需要把他们打一遍。但是这种第三方包大不说,它内部在我们编译时根本也不会发生变化。

那么我们将它们在第一次打包时打出来,然后链接到我们的文件中不好吗

这些提前打包好文件名叫做dll,其实就可以理解成一个缓存

怎样写一个缓存呢?思路可能是这样的

  • 先把东西存起来
  • 搞一个映射表,后面要用到时先查映射表中的东西,有则直接用

但是这玩意配置还是挺麻烦的,但是有个好消息。dll已经被废弃掉了

再但是既然写了这个标题还是搞一下作为了解吧

直接使用插件 autodll-webpack-plugin

安装npm i autodll-webpack-plugin

使用:

const path = require('path');
const AutoDllPlugin = require('autodll-webpack-plugin'); 

module.exports = {
  plugins: [
        // 配置要打包为 dll 的文件
        new AutoDllPlugin({
            inject: true, // 设为 true即 DLL bundles 插到 index.html 里
            filename: '[name].dll.js',
            context: path.resolve(__dirname, '../'), // AutoDllPlugin 的 context 必须和 package.json 的同级目录,要不然会链接失败
            entry: {
                react: [
                    'react',
                    'react-dom'
                ]
            }
        })
  ]
}

Dll之后的可以使用 HardSourceWebpackPlugin,比dll更快更简单

安装:npm i hard-source-webpack-plugin

使用

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  plugins: [
    new HardSourceWebpackPlugin() 
  ]
}

3.8 抽取公共代码

尤其是在一些多入口文件中,如入口index.js引用了a.js,又一个入口的other.js又引用了a.js,那么正常情况下a.js会被打印两次。嗯。。那可是没有必要的

抽取:

module.exports = {
  optimization: {
    splitChunks: {             // 分割代码块,针对多入口
      cacheGroups: {           // 缓存组
        common: {              // 公共模块
          minSize: 0,          // 大于0k抽离
          minChunks: 2,        // 被引用多少次以上抽离抽离
          chunks: 'initial'    // 从什么地方开始, 从入口开始
        }
      }
    }
  },
}

抽取一些像jquery这样的一般被多次引用的第三方包

optimization: {
  splitChunks: {              // 分割代码块,针对多入口
    cacheGroups: {            // 缓存组
      common: {               // 公共模块
        minSize: 0,           // 大于多少抽离
        minChunks: 2,         // 使用多少次以上抽离抽离
        chunks: 'initial'     // 从什么地方开始,刚开始
      },
      vendor: {
        priority: 1,          // 增加权重, (先抽离第三方)
        test: /node_modules/, // 把此目录下的抽离
        minSize: 0,           // 大于多少抽离
        minChunks: 2,         // 使用多少次以上抽离抽离
        chunks: 'initial'     // 从什么地方开始,刚开始
      }
    }
  },
},

3.8 webpack内置一些优化

自动了解吧,这里面没有啥东西

四:tapable ——手写早知道

tapable是一个类似于nodejseventEmitter的库,主要功能是控制各种钩子函数的发布与订阅,控制着webpack的插件系统

4.1 同步

4.1.1 SyncHook的用法与实现

最没有特点的一个hook,就是同步串行

const { SyncHook } = require('tapable')

class Lesson {
    constructor() {
        this.hooks = {
            arch: new SyncHook(['name']),
        }
    }
    start() {
        this.hooks.arch.call('gxb')
    }
    tap() {
        this.hooks.arch.tap('node', function(name) {
            console.log('node', name)
        })
        this.hooks.arch.tap('react', function(name) {
            console.log('react', name)
        })
    }
}


const l = new Lesson()

l.tap();
l.start()

// 实现
class SyncHook {
    constructor() {
        this.tasks = []
    }
    tap(name, cb) {
        let obj = {}
        obj.name = name
        obj.cb = cb
        this.tasks.push(obj)
    }
    call(...arg) {
        this.tasks.forEach(item => {
            item.cb(...arg)
        })
    }
}
const syncHook = new SyncHook()
syncHook.tap('node', name => {
    console.log('node', name)
})
syncHook.tap('vue', name => {
    console.log('vue', name)
})
syncHook.call('gxb')

4.1.2 SyncBailHook的用法与实现

/**
 * 订阅的处理函数有一个的返回值不是undefined就停止向下跑
 */
const { SyncBailHook } = require('tapable')

class Lesson {
    constructor() {
        this.hooks = {
            arch: new SyncBailHook(['name'])
        }
    }
    tap() {
        this.hooks.arch.tap('node', (name) => {
            console.log('node', name);
            return 'error'
        })
        this.hooks.arch.tap('vue', (name) => {
            console.log('vue', name);
            return undefined
        })
    }
    start() {
        this.hooks.arch.call('gxb')
    }


}
const l = new Lesson()
l.tap()
l.start()

/**
 * 方法实现
 */
class SyncBailHook {
    //一般是可以接收一个数组参数的,但是下面没有用到
    constructor(args) {
        this.tasks = []
    }
    tap(name, cb) {
        let obj = {}
        obj.name = name
        obj.cb = cb
        this.tasks.push(obj)
    }
    call(...arg) {
        let ret
        let index = 0
        do {
            ret = this.tasks[index++].cb(...arg)
        } while (ret === undefined && index < this.tasks.length);
    }
}

const syncBailHook = new SyncBailHook()
syncBailHook.tap('node', name => {
    console.log('node', name);
    return 'error'
})
syncBailHook.tap('vue', name => {
    console.log('vue', name);

})
syncBailHook.call('gxb')

4.1.3 SyncWaterfallHook的用法与与实现

/**
 * 上一个处理函数的返回值是下一个的输入
 */
/**
 * 订阅的处理函数有一个的返回值不是undefined就停止向下跑
 */
const { SyncWaterfallHook } = require('tapable')

class Lesson {
    constructor() {
        this.hooks = {
            arch: new SyncWaterfallHook(['name'])
        }
    }
    tap() {
        this.hooks.arch.tap('node', (name) => {
            console.log('node', name);
            return 'node ok'
        })
        this.hooks.arch.tap('vue', (data) => {
            console.log('vue', data);

        })
    }
    start() {
        this.hooks.arch.call('gxb')
    }


}
const l = new Lesson()
l.tap()
l.start()

/**
 * 方法实现
 */
class SyncWaterfallHook {
    //一般是可以接收一个数组参数的,但是下面没有用到
    constructor(args) {
        this.tasks = []
    }
    tap(name, cb) {
        let obj = {}
        obj.name = name
        obj.cb = cb
        this.tasks.push(obj)
    }
    call(...arg) {
        let [first, ...others] = this.tasks
        let ret = first.cb(...arg)
        others.reduce((pre, next) => {
            return next.cb(pre)
        }, ret)
    }
}

const syncWaterfallHook = new SyncWaterfallHook()
syncWaterfallHook.tap('node', data => {
    console.log('node', data);
    return 'error'
})
syncWaterfallHook.tap('vue', data => {
    console.log('vue', data);

})
syncBailHook.call('gxb')

4.1.4 SyncLoopHook的用法与实现

/**
 * 订阅的处理函数有一个的返回值不是undefined就一直循环它
 */
const { SyncLoopHook } = require('tapable')

class Lesson {
    constructor() {
        this.index = 0
        this.hooks = {
            arch: new SyncLoopHook(['name'])
        }
    }
    tap() {
        this.hooks.arch.tap('node', (name) => {
            console.log('node', name);
            return ++this.index === 3 ? undefined : this.index
        })
        this.hooks.arch.tap('vue', (name) => {
            console.log('vue', name);
            return undefined
        })
    }
    start() {
        this.hooks.arch.call('gxb')
    }


}
const l = new Lesson()
l.tap()
l.start()

/**
 * 方法实现
 */
class SyncLoopHook {
    //一般是可以接收一个数组参数的,但是下面没有用到
    constructor(args) {
        this.tasks = []
    }
    tap(name, cb) {
        let obj = {}
        obj.name = name
        obj.cb = cb
        this.tasks.push(obj)
    }
    call(...arg) {
        this.tasks.forEach(item => {
            let ret;
            do {
                ret = item.cb(...arg)
            } while (ret !== undefined);
        })
    }
}

const syncLoopHook = new SyncLoopHook()
let index = 0
syncLoopHook.tap('node', name => {
    console.log('node', name);
    return ++index === 3 ? undefined : index
})
syncLoopHook.tap('vue', name => {
    console.log('vue', name);

})
syncLoopHook.call('gxb')

4.2 异步

异步并发

4.2.1 AsyncParallelHook
/**
 * 异步并发,
 串行与并发的关系
 即并发需要把处理函数全部执行完再走最后的回调,而串行是一个处理函数执行完才走第二个
 */
const { AsyncParallelHook } = require("tapable")
class Lesson {
    constructor() {
        this.hooks = {
            arch: new AsyncParallelHook(['name'])
        }
    }
    tap() {

        this.hooks.arch.tapAsync('node', (name, cb) => {
            setTimeout(() => {
                console.log("node", name);
                cb();
            }, 1000);
        })

        this.hooks.arch.tapAsync('vue', (name, cb) => {
            // 使用宏任务
            setTimeout(() => {
                console.log('vue', name)
                cb()
            }, 1000)

        })


    }
    start() {
        this.hooks.arch.callAsync('gxb', function() {
            console.log('end')
        })
    }
}
let l = new Lesson();


l.tap();
l.start();


/**
 * 实现
 * 异步并发回忆特点,callAsync传入的回调是在所有异步任务执行完成之后执行的
 */
class SyncParralleHook {
    constructor() {
        this.tasks = []
    }
    tapAsync(name, cb) {
        let obj = {}
        obj.name = name
        obj.cb = cb
        this.tasks.push(obj)
    }
    //主要就是每一个异步处理函数执行完都在跑一下done,看是否该执行最后的回调了吗
    callAsync(...arg) {
        // 先拿出callAsync传过来的回调
        const lastCb = arg.pop()

        // 开始执行其他的异步任务
        let index = 0

        // done函数用来判断是不是要去执行lastCb了
        const done = () => {
            //根据异步函数的数量来执行lastCb
            index++
            if (index === this.tasks.length) {
                lastCb()
            }
        }
        this.tasks.forEach(item => item.cb(...arg, done))
    }
}
const hook = new SyncParralleHook()
hook.tapAsync('node', (name, cb) => {
    setTimeout(function() {
        console.log('node', name)
        cb()
    }, 1000)
})
hook.tapAsync('vue', (name, cb) => {
    setTimeout(function() {
        console.log('vue', name)
        cb()
    }, 1000)
})
hook.callAsync('gxb', function() {
    console.log('end')

})


/**
 * 处理函数是用微任务promis,即处理函数可以不传回调cb但是需要返回一个promise
 */
const { AsyncParallelHook } = require('tapable')
class Lesson {
    constructor() {
        this.hooks = {
            arch: new AsyncParallelHook(['name'])
        }
    }
    start() {
        this.hooks.arch.promise('gxb').then(function() {
            console.log('end')
        })
    }
    tap() {
        this.hooks.arch.tapPromise('node', name => {
            return new Promise((resove, reject) => {
                console.log('node', name)
                resove()
            })
        })
        this.hooks.arch.tapPromise('vue', name => {
            return new Promise((resove, reject) => {
                console.log('vue', name)
                resove()
            })
        })
    }
}

const l = new Lesson()
l.tap()
l.start()


/**
 * 实现
 * tapPromise
 * promise
 */
class AsyncParallelHook {
    constructor() {
        this.tasks = []
    }
    tapPromise(name, cb) {
        let obj = {}
        obj.name = name
        obj.cb = cb
        this.tasks.push(obj)
    }
    promise(...arg) {
        // task中的处理函数部分返回值都是promise,将它们映射一个数组中。使用promise的all全部执行他们
        const newTasks = this.tasks.map(item => item.cb(...arg))
        return Promise.all(newTasks)
    }
}
// 测试
const hook = new AsyncParallelHook()
hook.tapPromise('node', name => {
    return new Promise((res, rej) => {
        console.log('node', name)
        res()
    })
})
hook.tapPromise('vue', name => {
    return new Promise((res, rej) => {
        console.log('vue', name)
        res()
    })
})
hook.promise('gxb').then(function() {
    console.log('end')
})
4.2.2AsyncParallelBailHook

和同步同理

异步串行

4.2.3 AsyncSeriesHook
/**
 * 异步串行
 * 一个by一个的执行
 * 
 */

const { AsyncSeriesHook } = require('tapable')

class Lesson {
    constructor() {
        this.hooks = {
            arch: new AsyncSeriesHook(['name'])
        }
    }
    start() {
        this.hooks.arch.callAsync('gxb', function() {
            console.log('end')
        })
    }
    tap() {
        this.hooks.arch.tapAsync('node', (name, cb) => {
            setTimeout(() => {
                console.log('node', name)
                cb()
            }, 1000)
        })
        this.hooks.arch.tapAsync('vue', (name, cb) => {
            setTimeout(() => {
                console.log('node', name)
                cb()
            }, 1000)
        })
    }
}
const l = new Lesson()
l.tap()
l.start()


/**
 * 实现
 * 特点:一个函数执行完再执行再一个
 */
class AsyncSeriesHook {
    constructor() {
        this.tasks = []
    }
    tapAsync(name, cb) {
        let obj = {}
        obj.name = name
        obj.cb = cb
        this.tasks.push(obj)
    }
    callAsync(...arg) {
        const finalCb = arg.pop()
        let index = 0
        let next = () => {
            if (this.tasks.length === index) return finalCb()

            let task = this.tasks[index++].cb
            task(...arg, next)
        }
        next()
    }
}

const hook = new AsyncSeriesHook()
hook.tapAsync('node', (name, cb) => {
    setTimeout(() => {
        console.log('node', name)
        cb()
    }, 1000)
})
hook.tapAsync('vue', (name, cb) => {
    setTimeout(() => {
        console.log('vue', name)
        cb()
    }, 1000)
})
hook.callAsync('gxb', function() {
    console.log('end')
})
4.2.4 AsyncSeriesBailHook

参考同步

4.2.5 AsyncSeriesWaterfallHook

参考同步

五:手写一个简单的webpack

5.1 构建入口文件,即link到本地

初始化一个新的项目,首先npm init -y生成package.json

写一个bin命令

 "bin": {
        "mypack": "./bin/index.js"
    },

./bin/index.js作为我们的入口文件

再运行npm link, 将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试

再创建一个写了webpack.config.js文件的项目(先称之为要打包的项目文件吧——>源码项目)。运行命令`npm link mypack

这是在要打包的项目中运行命令 npx mypack,其所用的就是我们手写的那个了

5.2 构建核心,compiler类的编写

回到手写webpack项目下初始化compiler类

入口文件中没什么东西

只是需要拿到源码项目下的webpack.config.js,接下来调用compiler方法将拿到的配置文件地址传进去

#!  /usr/bin/env node

const path = require('path')

//   拿配置文件
const config = require(path.resolve('webpack.config.js'))
const Compiler = require('../lib/compiler.js')

const compiler = new Compiler(config)
compiler.run()

Compiler类的基本骨架

const path = require('path')
const fs = require('fs')
const tapable = require('tapable')
class Compiler {
    constructor(config) {
        this.config = config
        this.entryId = ''
        this.modules = {}
        this.entry = config.entry
        this.root = process.cwd() //拿到当前项目地址      
        this.asserts = {} // 存储chunkName与输出代码块
        this.hooks = {
            entryInit: new tapable.SyncHook(),
            beforeCompile: new tapable.SyncHook(),
            afterCompile: new tapable.SyncHook(),
            afterPlugins: new tapable.SyncHook(),
            afteremit: new tapable.SyncHook(),
        }

        const plugins = this.config.plugins
        if (Array.isArray(plugins)) {
            plugins.forEach(item => {
                // 每个均是实例,调用实例上的一个方法即可,传入当前Compiler实例
                item.run(this)
            })
        }
    }

    // 构建模块
    buildMoudle(modulePath, isEntry) {}
     //写的输出位置
    emitFile() {}
      run() {
        this.hooks.entryInit.call()
        this.buildMoudle(path.resolve(this.root, this.entry), true)
        this.hooks.afterCompile.call()
        this.emitFile()
    }
}

构建模板

首先run中传过来的是一个入口地址,和一个标志是不是入口的布尔。我们要做的是首先入口的相对地址赋值给this.entryId

然后获取文件源码,搞成{文件相对地址:改造后源码}这种键值对的形式

   // 构建模块
    buildMoudle(modulePath, isEntry) {
        let source = this.getSource(modulePath)

        // 其实就是this.entry 啊
        let moduleName = './' + path.relative(this.root, modulePath)
        if (isEntry) {
            this.entryId = moduleName
        }

        // 开始改造源码,主要针对require
        let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) //就是./src

        // 改造完成放入模块中
        this.modules[moduleName] = sourceCode

        // 递归构造依赖模板
        dependencies.forEach(item => {
            this.buildMoudle(path.resolve(this.root, item), false)
        })

    }
// 获取源码
    getSource(modulePath) {
        // 事先拿module中的匹配规则与路径进行匹配

        let content = fs.readFileSync(modulePath, 'utf8')

        return content
    }

改造源码

这里要用到一些工具

  • babylon 将源码搞成AST抽象语法树
  • @babel/traverse用于替换AST上的节点
  • @babel/generator 结果生成
  • @babel/types AST节点的Lodash-esque实用程序库

安装:npm i babylon @babel/traverse @babel/types @babel/generator

源码:就是将require换成__webpack_require__,同时在改造一下require的内部路径参数。其实webpack可是打包require本质就是手动实现了一个require方法

注意:本次只是重写了require,可仍是不支持es6模块的啊

// 源码改造
    parse(source, parentPath) {
        // 生成ast语法树
        let ast = babylon.parse(source)

        // 用于存取依赖
        let dependencies = []
        traverse(ast, {
            CallExpression(p) {
                let node = p.node
                    // 对require方法进行改名
                if (node.callee.name === 'require') {
                    node.callee.name = '__webpack_require__'
                    let moduledName = node.arguments[0].value // 取到require方法里的路径

                    // 改造其后缀名
                    moduledName = moduledName + (path.extname(moduledName) ? '' : '.js')

                    // 加上其父路径./src
                    moduledName = './' + path.join(parentPath, moduledName)

                    // 加入依赖数组
                    dependencies.push(moduledName)

                    // 源码替换
                    node.arguments = [type.stringLiteral(moduledName)]
                }
            }
        })

        let sourceCode = generator(ast).code
        return { sourceCode, dependencies }
    }

输出到output的指定位置

//写到输出位置
    emitFile() {

        // 拿到输出地址
        let outPath = path.join(this.config.output.path, this.config.output.filename)

        // 拿模板
        let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))

        // 填充模板数据
        let code = ejs.render(templateStr, {
            entryId: this.entryId,
            modules: this.modules
        })
        this.asserts[outPath] = code

        console.log(code);


        // 写入
        fs.writeFileSync(outPath, this.asserts[outPath])
    }
(function (modules) {
    var installedModules = {};

    function __webpack_require__(moduleId) {

    if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
    }
    var module = installedModules[moduleId] = {
    i: moduleId,
    l: false,
    exports: {}
    };

    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    module.l = true;

    return module.exports;
    }


    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = "<%-entryId %>");
    })({
    <% for(let key in modules){ %>
        "<%- key %>":
        (function (module, exports,__webpack_require__) {
    eval(`<%-modules[key] %>`);
    }),
    <% } %>
    });

加入loader

写一个less-loader和style-loader,css-loader只是处理一些css中的@import的语法,故这里为求简便不写css-loader了

less-loader:注要就是借用less,将less文件转为css文件

const less = require('less')

function loader(source) {
    let css = ''
    less.render(source, function(err, output) {
        css = output.css
    })

    css = css.replace(/\n/g, '\\n')
    return css
}
module.exports = loader

style-loader:搞一个标签,将css放进去

function loader(source) {
    let style = `
    let style = document.createElement('style')
    style.innerHTML = ${JSON.stringify(source)}
    document.head.appendChild(style)
    `
    return style
}
module.exports = loader

此时的webpack.config.js

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'main.js',
        path: path.join(__dirname, './dist')
    },
    module: {
        rules: [{
            test: /\.less$/,
            use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')]
        }]
    },
    plugins: [
        new TestPlugins(),
        new InitPlugin()
    ]
}

此时源码文件获取函数改造如下

// 获取源码
    getSource(modulePath) {
        // 事先拿module中的匹配规则与路径进行匹配
        const rules = this.config.module.rules
        let content = fs.readFileSync(modulePath, 'utf8')

        for (let i = 0; i < rules.length; i++) {
            let { test, use } = rules[i]
            let len = use.length

            // 匹配到了开始走loader,特点从后往前
            if (test.test(modulePath)) {
                console.log(111);

                function normalLoader() {
                    // 先拿最后一个
                    let loader = require(use[--len])
                    content = loader(content)

                    if (len > 0) {
                        normalLoader()
                    }
                }
                normalLoader()
            }
        }
        return content
    }

并添加插件

直接在源码项目的webpack.config.js中编写吧

const path = require('path')
class TestPlugins {
    run(compiler) {
        // 将自身方法订阅到hook以备使用
        //假设它的运行期在编译完成之后
        compiler.hooks.afterCompile.tap('TestPlugins', function() {
            console.log(`this is TestPlugins,runtime ->afterCompile `);

        })
    }
}

class InitPlugin {
    run(compiler) {
        // 将的在执行期放到刚开始解析入口前
        compiler.hooks.entryInit.tap('Init', function() {
            console.log(`this is InitPlugin,runtime ->entryInit `);
        })
    }
}
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'main.js',
        path: path.join(__dirname, './dist')
    },
    module: {
        rules: [{
            test: /\.less$/,
            use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')]
        }]
    },
    plugins: [
        new TestPlugins(),
        new InitPlugin()
    ]
}

其实手写逻辑还是比较难说清楚的,还不如直接看代码,下面我将compiler类的全部代码粘了出来,其中注释已经写得自我感觉很详细了

compiler类全部代码

const path = require('path')
const fs = require('fs')
const { assert } = require('console')
    // babylon  主要把源码转成ast Babylon 是 Babel 中使用的 JavaScript 解析器。
    // @babel/traverse 对ast解析遍历语法树 负责替换,删除和添加节点
    // @babel/types 用于AST节点的Lodash-esque实用程序库
    // @babel/generator 结果生成

const babylon = require('babylon')
const traverse = require('@babel/traverse').default;
const type = require('@babel/types');
const generator = require('@babel/generator').default
const ejs = require('ejs')
const tapable = require('tapable')

class Compiler {
    constructor(config) {
        this.config = config
        this.entryId = ''
        this.modules = {}
        this.entry = config.entry
        this.root = process.cwd() //当前项目地址      
        this.asserts = {} // 存储chunkName与输出代码块
        this.hooks = {
            entryInit: new tapable.SyncHook(),
            beforeCompile: new tapable.SyncHook(),
            afterCompile: new tapable.SyncHook(),
            afterPlugins: new tapable.SyncHook(),
            afteremit: new tapable.SyncHook(),
        }

        const plugins = this.config.plugins
        if (Array.isArray(plugins)) {
            plugins.forEach(item => {
                // 每个均是实例,调用实例上的一个方法即可,传入当前Compiler实例
                item.run(this)
            })
        }
    }

    // 获取源码
    getSource(modulePath) {
        // 事先拿module中的匹配规则与路径进行匹配
        const rules = this.config.module.rules
        let content = fs.readFileSync(modulePath, 'utf8')

        for (let i = 0; i < rules.length; i++) {
            let { test, use } = rules[i]
            let len = use.length

            // 匹配到了开始走loader,特点从后往前
            if (test.test(modulePath)) {
                console.log(111);

                function normalLoader() {
                    // 先拿最后一个
                    let loader = require(use[--len])
                    content = loader(content)

                    if (len > 0) {
                        normalLoader()
                    }
                }
                normalLoader()
            }
        }
        return content
    }

    // 源码改造
    parse(source, parentPath) {
        // ast语法树
        let ast = babylon.parse(source)

        // 用于存取依赖
        let dependencies = []
        traverse(ast, {
            CallExpression(p) {
                let node = p.node
                    // 对require方法进行改名
                if (node.callee.name === 'require') {
                    node.callee.name = '__webpack_require__'
                    let moduledName = node.arguments[0].value // 取到require方法里的路径

                    // 改造其后缀名
                    moduledName = moduledName + (path.extname(moduledName) ? '' : '.js')

                    // 加上其父路径./src
                    moduledName = './' + path.join(parentPath, moduledName)

                    // 加入依赖数组
                    dependencies.push(moduledName)

                    // 源码替换
                    node.arguments = [type.stringLiteral(moduledName)]
                }
            }
        })

        let sourceCode = generator(ast).code
        return { sourceCode, dependencies }
    }

    // 构建模块
    buildMoudle(modulePath, isEntry) {
        let source = this.getSource(modulePath)

        // 其实就是this.entry 啊
        let moduleName = './' + path.relative(this.root, modulePath)
        if (isEntry) {
            this.entryId = moduleName
        }

        // 开始改造源码,主要针对require
        let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) //就是./src

        // 改造完成放入模块中
        this.modules[moduleName] = sourceCode

        // 递归构造依赖模板
        dependencies.forEach(item => {
            this.buildMoudle(path.resolve(this.root, item), false)
        })

    }

    //写的输出位置
    emitFile() {

        // 拿到输出地址
        let outPath = path.join(this.config.output.path, this.config.output.filename)

        // 拿模板
        let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))

        // 填充模板数据
        let code = ejs.render(templateStr, {
            entryId: this.entryId,
            modules: this.modules
        })
        this.asserts[outPath] = code

        console.log(code);


        // 写入
        fs.writeFileSync(outPath, this.asserts[outPath])
    }

    run() {
        this.hooks.entryInit.call()
        this.buildMoudle(path.resolve(this.root, this.entry), true)
        this.hooks.afterCompile.call()
        this.emitFile()
    }

}

module.exports = Compiler

/**
 * 到此为止一个简单的webpack就已经完成了
 */

写到最后,到了这里webpack应该算是正式入了门。下面的东西留到以后总结吧。脑图就不上传了哈

参考致谢:

webpack系列之二Tapable

轻松通过两个实例来理解 webpack 中的 devtool: 'source-map' 是什么意思

这才是官方的tapable中文文档

npm link用法总结

如何开发webpack loader

玩转webpack

搞定webpack4

webpack实战:入门、进阶与调优

本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/nhZNvKd_L8GoGjr00Nxbcw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录