Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack读书小结 #72

Open
adodo0829 opened this issue May 22, 2020 · 1 comment
Open

webpack读书小结 #72

adodo0829 opened this issue May 22, 2020 · 1 comment

Comments

@adodo0829
Copy link
Owner

adodo0829 commented May 22, 2020

webpack读书小结

本篇主要是针对 webpack 文档的读书笔记, 以目录的形式展现, 旨在对 webpack 有个全局的了解...使用的时候不至于那么陌生emm...持续更新中ing

1.webpack简介

学一个知识点或者某一领域的内容, 遵守这个知识点是什么(what); 为什么用它(why); 怎么用它(how); 什么场景下用它(who); 使用时候的注意事项,坑点(when),当然这是事后总结...这是我一贯的学习习惯

  • 1.webpack是什么(what)
webpack: js模块打包工具, 可以把各个存在依赖关系的模块,
按照特定的规则和顺序组织在一起,最终输出为一个JS文件(或者多个)
  • 2.webpack的使用场景(who)
应用: 大型项目由多个文件模块组成, 当模块代码依赖过多时, 手动引入不利于维护
webpack 可以将多个模块按照依赖关系自动打包, 维护方便, 提升项目开发效率
  • 3.webpack怎么用(how)
# 我们选择在项目全部安装 webpack
yarn init
yarn add webpack webpack-cli -D
# 创建打包入口文件index.js
# 创建打包配置文件webpack.config.js
# 更多配置项见官方文档
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  module: 'development',
  devServer: {
    publicPath: '/dist'
  }
}
# 在 package.json中定义脚本命令
{
  "script": {
    "build": "webpack",
    "dev": "webpack-dev-server"
  }
}
  • 4.为啥用webpack(why)
1.Webpack默认支持多种模块标准,包括AMD、CommonJS,以及ESModule
2.Webpack有完备的代码分割(code splitting)解决方案, 异步加载(jsonp)
3.Webpack可以处理各种类型的资源, 丰富的 loader 插件
4.Webpack 生态圈比较完善, 很多踩过的坑已经被别人踩了

2.模块简介

webpack要处理的对象: 模块
这里主要介绍一下主流的模块化标准: CommonJS 规范和 ESModule 规范, 以及他们的特性

  • CommonJS规范
    js文件即模块
作用域: 模块内部本身

导出: 模块内部有个 moudle 对象(记录每个模块的状态), 有个exports属性,用来导出
     module.exports(他也是一个引用) = {}, 
     或者exports.xxx = ooo(exports引用module.exports)

导入: require('./module.js')
     require(): 它是一个函数, 执行()中的路径参数, 从而去执行路径模块文件的内容
     再次调用(module.loaded = true): 直接去上次调用后缓存的结果
  • ESModule规范
    他是js语言级别的特性,有特定关键字语法实现,在解析编译时确定模块(引用);
    同样也是文件即模块, 默认严格模式
作用域: 模块自身

导出: export 语法
     具名导出: export const name = 'huhua', export 变量, export { name as 改个名导出 }
     默认导出: export defalut 你想导出的变量或者确定值; 模块挂载 default 属性上

导入: import 语法
     导入具名模块: import { name } from './info.js'
     导入默认模块: import Name from './infoDefault.js'
     整体导入(当模块导出多个变量时): import * as ModuleObj from './info.js'
     复合导入: import XXX, { ooo } from 'module'
     ...不说了
     自执行导入: import './autoDo.js'
  • 二者的区别
1.加载方式:
  CommonJS: 在代码运行时去查找,运行模块依赖文件(动态的)
  ESModule: 在代码解析,编译阶段查找模块依赖文件(静态的, 针对这一特性: 便有了tree shaking等一系列优化措施) 

2.导入结果:
  CommonJS: 导入的是一个模块执行结果的拷贝
  ESModule: 导入的是一个模块属性的只读引用

3.循环依赖解决: 项目太大时, 代码可能存在隐式的模块依赖循环引用
  比如: a.js依赖b.js, b.js又依赖a.js
  CommonJS: 执行逻辑: a -> b(引入a: 直接导出a,未执行完: 空{}, 继续执行b) -> b 执行完在执行 a
  解决: 模块是值拷贝, 无法解决, 只能提前返回一个{}
  ESModule: 执行逻辑: a -> b(引入a: 直接导出a,未执行完: undefined, 继续执行b) -> b 执行完在执行 a
  解决: 因为模块是动态引用, 利用这个特性可以解决
  • UMD: 通用模块标准, 根据执行环境运行模块
(function (global, main) {
  if (typeof define === 'function' && define.amd) {
    // AMD module
    define(fn())
  } else if (typeof exports === 'object') {
    // commonjs module
    module.exports = { fn }
  } else {
    // 浏览器环境
    global.fn = fn
  }
})(this, function() { return { /*模块 */ } })
  • npm 模块
    npm: js 的工具包管理器
# 初始化项目
npm init -y
# 安装包
npm install package
# 这些包会被安装在 node_modules 文件夹下, 在项目导入是会去自动搜索
# 每个 package 在自己的 package.json 下又有一个入口 main: 'name.js'
  • webpack模块打包结果大致结构
(function (modules) {
  var installedModules = {} // 缓存已加载模块

  // 模块加载函数
  function __webpack__require__(moduleId) {
    // 判断即将加载的模块是否存在于installedModules中
    // 如果存在则直接取值, 不存在则去获取module.exports的值
  }

  // 执行入口模块
  return __webpack__require__(__webpack__require__.s = 0)
})(
  // 模块对象集合
  {
    0: function(module, exports, __webpack__require__) {
      // 打包的入口
      module.exports = __webpack__require__('1') // 下一个模块的 id
    },
    1: function(module, exports, __webpack__require__) {
      // 模块 1 逻辑
    },
    2: function(module, exports, __webpack__require__) {
      // 模块 2 逻辑
    },
    // ...
  }
)

3.webpack输入和输出

webpack会从入口文件 entry 所指文件开始检索, 将具有依赖关系的模块生成一颗依赖树,
最终得到一个 chunk, 即打包好的 bundle.js文件

输入: 入口配置

module.exports = {
  // 工程根目录路径
  context: path.join(__dirname, './src') 
  // 字符串路径
  entry: './index.js' 
  // 数组:资源预先合并(导入), 末尾为入口
  entry: ['babel-polyfill', './index.js']
  // entry为对象时, 用于定义多入口
  entry: {
     index: './index.js',
     lib: './lib/index.js'
  }
  // entry 为函数时, 返回值为上面配置即可
  entry: () => {
    return {
      index: './index.js',
      lib: './lib/index.js'
    }
  }
}
  • 应用打包
// 单页应用打包
module.exports = {
  entry: {
    index: './index.js', // 业务 chunk
    vendor: ['vue', 'vue-router'] // 第三方模块打包的chunk, 
  }
}

// 多页应用
module.exports = {
  entry: {
    page1: './src/page1.js',
    page2: './src/page2.js',
    page3: './src/page3.js',
    vendor: ['vue', 'vue-router'] // 公共第三方模块, 需配合optimization.splitChunks
  }
  // 打完包后, 各自的 html 引入对应的 chunk 即可
}

输出: 出口文件

// 单入口
module.exports = {
  entry: './src/index.js',
  // 配置输出对象
  output: {
    filename: 'bundle.js', // 输出文件名
    path: path.join(__dirname, 'dist'), // 项目资源输出的目录
    publicPath: '/dist/' // 指定打包资源的请求路径
    // 如果我们打包后文件不是放在根域名下, 需要特别注意一下,曾经踩过坑

    // 1.资源文件在 html 中加载, publicPath表示index.html所在的相对路径的拼接
    // 假设 index.html资源访问路径为 http://xxx.com/dist/index.html
    // 引入的js文件名为 chunk0.js
    publicPath: '' // 访问路径为: http://xxx.com/dist/chunk0.js
    publicPath: './js' // 访问路径为: http://xxx.com/dist/js/chunk0.js
    publicPath: '../assets/' // 访问路径为: http://xxx.com/assets/chunk0.js

    // 2.publicPath为 /: 以当前的 host 为基础路径 进行拼接
    publicPath: '/' // http://xxx.com/chunk0.js
    publicPath: '/js' // http://xxx.com/js/chunk0.js

    // 3.CDN 路径, 拼接 cdn 绝对路劲
    publicPath: 'https://cdn1.com/' // https://cdn1.com/chunk0.js

    // 开发环境下: webpack-dev-server 也有一个配置, 表示静态资源的位置
    devServer: {
      publicPath: '/dist/',
      port: 8080
    }
  }
}

// 多入口时
module.exports = {
  entry: {
    app: './src/index.js',
    lib: './src/lib.js'
  },
  output: {
    // 动态生成输出文件名, 对应上面的chunk name
    // 生成环境下加上hash值, 可用于清除静态资源缓存
    filename: '[name].js' // [name].[chunkhash].js
  }
}

4.webpack 预处理器 loader: 处理各类资源文件模块

我们一个工程目录下往往并不是只有 js 文件, 还有其他资源文件, 如 html, css,图片,字体等...
他们又不是标准的模块, 那我们打包怎么处理, 所以引入预处理器 loader 来负责.

loader 简介

loader本质上是一个函数

output = loader(input)
// 支持链式调用, 可以理解为管道操作
output = loader1(loader2(loader3(input)))

// 示例
function loader(content, map, meta) {
  var cb = this.async()
  var result = handler(content, map, meta) // 处理资源
  cb(
    null,
    result.content, // 处理后的内容
    result.map,     // 处理后的 map
    result.meta     // 处理后的 AST
  )
}

loader 配置

module.exports = {
  // 省略...
  module: {
    rules: [
      {
        // 处理 .css类型的模块文件
        test: /\.css$/,     
        // 使用哪些laoder处理, loader 机制从后往前处理
        use: [
          'style-loader', // loader1
          {
            loader: 'css-loader',
            // options 传入自己的配置
            options: {

            },
          }.
          // 指定使用 loader 的目录
          exclude: '/src\/lib/', // src/lib下不使用, 优先级高
          include: '/src/'       // src 下都使用

          // resource 和 issuer 可以更精确控制加载的文件使用loader
        ],
      }
    ]
  }
}

项目常用 loader

  • babel-loader
// babel-loader 用来处理 es6+ 并编译为 es5
rules: [
  {
    test: '/\.js$/',
    exclude: '/node_modules/',
    use: {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true, // 缓存打包文件, 减少二次编译
        presets: [ // 设置目标环境
          ['env', { modules: false }]
        ]
      }
    }
  }
]

// babel-loader 支持.babelrc 文件读取 babel 配置, 可以抽离出来
{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2",
  ],
  "plugins": ["transform-runtime"]
}
// 也支持 babel.config.js
module.exports = {
  presets: [
    '@vue/app'
  ]
}
  • ts-loader
// 打包 ts 文件
rules: [
  {
    test: /\.ts$/,
    use: 'ts-loader'
  }
]
// 项目根目录下配置 tsconfig.json
// 具体去参考官网
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "allowJs": true,
    "sourceMap": true,
    "baseUrl": ".",
    "types": [
      "node",
      "jest",
      "webpack-env"
    ],
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}
  • html-loader
// HTML文件转化为字符串并进行格式化,把一个HTML片段通过JS加载进来
rules: [
  {
    test: '/\.html$/',
    use: 'html-loader'
  }
]

import aHtml from './a.html'
document.write(aHtml)
  • file-loader
// 打包文件资源, 返回 publicPath 资源引用路径
rules: [
  {
    test: '/\.(png|jpg|gif)$/',
    use: {
      loader: 'file-loader',
      options: {
        name: '[name].[ext]',
        publicPath: './assets/' // 默认与工程的 publicPath一致
      }
    }
  }
]

import imgPath from './images/xx.png'
console.log(imgPath) // ./assets/hash.png
  • url-loader
// 文件转 base64 值
rules: [
  {
    test: '/\.(png|jpg|gif)$/',
    use: {
      loader: 'url-loader',
      options: {
        limit: 10240, // 小于就转
        name: '[name].[ext]',
        publicPath: './assets/' // 默认与工程的 publicPath一致
      }
    }
  }
]

import imgPath from './images/xx.png'
console.log(imgPath) // data:image/png base64字符
  • vue-loader
<!-- 处理 .vue 文件 -->
<template>
  <div> {{ msg }} </div>
</template>
<script>
export default {
  data() {
    return {
      msg: 'vue'
    }
  } 
}
</script>
<style lang="css">
div {
  color: red
}
</style>

<!--
rules: [
  {
    test: '/\.vue$/',
    use: 'vue-loader'
  }
]
-->

自定义的 loader 函数

loader API

function someloader(content) {
  // content: 资源文件(resource file)的内容
  function dosomething(file) {
    // ....
  }
  // 1.同步 loader
  return dosomethig(content)

  // 2.异步 loader
  var cb = this.async()
  someAsyncOperation(content, function(err, result) {
    if (err) return callback(err);
    callback(null, result, map, meta);
  });
}

5.webpack 插件 Plugins

Plugins 用于 bundle 文件的优化,资源管理和环境变量注入,在整个构建过程起作用

const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 用于访问内置插件

const config = {
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  },
  plugins: [
    // 使用
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

module.exports = config;
// 常用插件
// CommonsChunkPlugin 将 chunks 相同的模块代码提取成公共 js
// CleanWebpackPlugin 清理构建目录
// ExtractTextWebpackPlugin 将 CSS 从 bundle 文件中提取成一个独立的 CSS 文件
// CopyWebpackPlugin 将文件或者文件夹拷贝到构建的输出目录
// HTMLWebpackPlugin 创建 html 文件去承输出的 bundle
// UglifyjsWebpackPlugin 压缩 JS
// ZipWebpackPlugin 将打包出的资源生成一个 zip 包

6.代码分片

也就是将打包的代码分成小块, 在访问页面时加载必要的资源, 其他资源可以延迟加载或者渐进式的加载...
说了那么多, 即按需加载必要代码

怎么去划分和管理代码块

  • 1.通过入口
entry: {
  app: './src/index.js', // 业务代码
  lib: ['./lib/lib.js']        // 工具库
}
// 引入到 html 中,
// dist/lib.js dist/app.js
  • 2.通过插件CommonChunkPlugin && SplitChunks
// 可以将多个Chunk中公共的部分提取出来
// 开发过程中减少了重复模块打包,可以提升开发速度
// 减小整体资源体积
// 合理分片后的代码可以更有效地利用客户端缓存
new webpack.optimize.CommonsChunkPlugin({
  // https://www.webpackjs.com/plugins/commons-chunk-plugin/#%E9%85%8D%E7%BD%AE
})

// SplitChunks
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.js',
    publicPath: '/dist/'
  },
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}

// optimization.splitChunks默认配置
splitChunks: {
  chunks: "async", // 工作模式: 提取异步 chunk
  // chunk匹配条件
  minSize: 30000,
  minChunks: 1,
  maxAsyncRequests: 5,
  maxInitialRequests: 3,
  automaticNameDelimiter: '~', // 分隔符
  name: true,
  // chunk 分离规则cacheGroups && default
  cacheGroups: {
    vendors: {
      // 作用于所有node_modules中符合条件的模块
      test: /[\\/]node_modules[\\/]/,
      priority: -10
    },
  default: {
      // 作用于被多次引用的模块
      minChunks: 2,
      priority: -20,
      reuseExistingChunk: true
    }
  }
}
  • 按需异步加载文件: import函数
    通过import函数加载的模块及其依赖会被异步地进行加载,并返回一个Promise对象
// loading.js
export function start() {
  console.log('page is loading')
}

import('./loading.js').then(({ start }) => {
  start()
})
// 通过js在页面的head标签里插入一个script标签/dist/chunkName.js

// webpack.config.js
output: {
    chunkFilename: ('[name].js') //指定异步chunk的文件名
  },

import(/* webpackChunkName: 'start' */ './start.js')
.then((module) => {
  module.start()
})

生产环境打包配置注意

1.环境变量:
通过判断 process.env.NODE_ENV
2.source map 配置
3.代码压缩 uglify(js,css)
4.缓存文件更新, hash name设置
5.动态 html改变, htmlWebpackPlugin
6.bundle.js 体积大小监控

项目构建打包优化

  • 1.happypack插件
    happypack替换初始的loader. 开启多线程进行转译,意味着要消耗 cpu 资源
module.exports = {
  module: {
    rules: [
      {
        test: '/\.js$/',
        exclude: '/node_modules/',
        loader: 'happypack/loader?id=js'
      },
      {
        test: '/\.ts$/',
        exclude: '/node_modules/',
        loader: 'happypack/loader?id=ts'
      }
    ]
  },
  plugins: [
    new HappyPack({
      id: 'js',
      loaders: [
        loader: 'babel-loader',
        options: {}
      ]
    }),
    new HappyPack({
      id: 'ts',
      loaders: [
        loader: 'ts-loader',
        options: {}
      ]
    }),
  ]
}
  • 2.减少不必要的转译模块
exclude,include
ignorePlugin 插件
cache缓存
  • 3.tree shaking
    ES6 Module依赖关系的构建是在代码编译时而非运行时。基于这项特性Webpack提供了tree shaking功能,它可以在打包过程中帮助我们检测工程中没有被引用过的模块,这部分代码将永远无法被执行到,因此也被称为“死代码”。Webpack会对这部分代码进行标记,并在资源压缩时将它们从最终的bundle中去掉
// es module 特性:
// 只能作为模块顶层的语句出现
// import 的模块名只能是字符串常量
// import binding 是只读的

// 对 babel 编译的 AST 针对上述特性做处理
// 修改 AST

['@babel/preset-env', { module: false }]

本地开发优化

  • webpack-merge合并公共配置文件
  • 模块热替换 HMR
{
  // ....
  devServer: {
    hot: true
  }
}
// 核心: 资源文件更新,加载, chunk diff
// dev server: 监听文件更新, ws发送通知, 和文件 hash 值
// client: 校对hash 值, 发送变更资源的请求[hash].hot-update.json
@adodo0829
Copy link
Owner Author

add

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant