webpack知识点与踩坑点

18 篇文章 0 订阅

webpack知识点简介

这篇文章是关于几个月前对webpack一些知识的迁移,前段时间做项目直接用的配好的环境,虽然简单快捷,但也导致我对一些知识的淡忘和能力的下降,特此记录。

loader

Loader 可以看作具有文件转换功能的翻译员,配置里的module.rules数组配置了一组规则,告诉 Webpack 在遇到哪些文件时使用哪些 Loader 去加载和转换。

css-loader style-loader

  • css-loader读取 CSS 文件
  • style-loader把 CSS 内容注入到 JavaScript 里

file-loader url-loader

  • file-loader
    简单讲:就是为打包生成的文件匹配文件路径
  • url-loader (更强大
    简单讲:小图片base64编码加入到代码里面,一般有limit,base64长度超过limit就转file-loader了
    • 获取 limit 参数
    • 如果 文件大小在 limit 之内,则直接返回文件的 base64 编码后内容
    • 如果超过了 limit ,则调用 file-loader处理

style-resources-loader

  1. 导入公共样式文件,避免在每个组件使用的样式文件里面单独使用@import引入
  2. link和@import之间的区别
    • link 是 HTML 文件中的标签,在 标签中引入 CSS 文件。
    • @import 是 CSS 中的一个 @规则,只能出现在 CSS 文件中或 HTML文件的

plugin

HtmlWebpackPlugin

html-webpack-plugin详解
html-webpack-plugin用法全解

  1. 作用
  • 生成入口html文件,单页应用只有一个index.html,想配置多个则可以配置N个html-webpack-plugin
  • 自动引入assets,并且如果hash设为true,就会自动添加每次webpack编译的hash值
  1. 还记得我的这篇文章吗?react学习笔记系列(一)在学习webpack的时候有关++webpack-dev-server打包的main.js托管到内存里,在计算机磁盘目录看不到,使用html-webpack-plugin生成的html文件会自动把这个main.js引入进来,并且将index.html(新生成的html文件)也挂到内存上++。这个知识点记忆尤深,其实生产环境就不用webpack-dev-server了所以讲html-webpack-plugin的作用时很多不会提到这一点。
  2. 一些使用:
配置多个html页面

实例化该插件多次即可:

 ...
    plugins: [
        new HtmlWebpackPlugin({
             template: 'src/html/index.html',
              excludeChunks: ['list', 'detail']
        }),
        new HtmlWebpackPlugin({
            filename: 'list.html',
            template: 'src/html/list.html',
            thunks: ['common', 'list']
        }), 
        new HtmlWebpackPlugin({
          filename: 'detail.html',
          template: 'src/html/detail.html',
           thunks: ['common', 'detail']
        })
    ]
    ...

这样会生成三个入口页面:index.html、list.html、detail.html;并且每个页面注入的thunk不尽相同;类似如果多页面应用,就需要为每个页面配置一个;

配置自定义模版
  • 不配置参数的HtmlWebpackPlugin默认生成的html只是将thunk和css插入到文档中,没配置loader时默认使用ejs模版引擎。
  • 例如项目中有2个入口html页面,它们可以共用一个模板文件,利用ejs模板的语法来动态插入各自页面的chunk和css样式,代码可以这样:
<!DOCTYPE html>
<html style="font-size:20px">
<head>
    <meta charset="utf-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <% for (var css in htmlWebpackPlugin.files.css) { %>
    <link href="<%=htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
    <% } %>
</head>
<body>
<div id="app"></div>
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
<script type="text/javascript" src="<%=htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
<% } %>
</body>
</html>

其他内容待补充

有关于webpack 的dll文件

dll文件是什么?(百度百科)

DLL(Dynamic Link Library)文件为动态链接库文件,又称“应用程序拓展”,是软件文件类型。在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。一个应用程序可使用多个DLL文件,一个DLL文件也可能被不同的应用程序使用,这样的DLL文件被称为共享DLL文件。

以上是百科中dll文件的介绍:而webapck的dll是借鉴了Windows系统的dll。一个dll包,就是一个纯纯的依赖库,它本身不能运行,是用来给你的app引用的。打包dll的时候,Webpack会将所有包含的库做一个索引,写在一个manifest文件中,而引用dll的代码(dll user)在打包的时候,只需要读取这个manifest文件,就可以了。

  1. 好处:
  • Dll打包以后是独立存在的,只要其包含的库没有增减、升级,hash也不会变化,因此线上的dll代码不需要随着版本发布频繁更新。
  • App部分代码修改后,只需要编译app部分的代码,dll部分,只要包含的库没有增减、升级,就不需要重新打包。这样也大大提高了每次编译的速度。
  • 假设你有多个项目,使用了相同的一些依赖库,它们就可以共用一个dll。

vendors是什么

vendors是利用CommonsChunkPlugin插件抽出公用模块打包出来的文件。(在使用的时候发现如果不对整个项目打包但是这个也是已经打包好的。)

  • CommonsChunkPlugin主要是用来提取第三方库和公共模块,避免首屏加载的bundle文件或者按需加载的bundle文件体积过大,从而导致加载时间过长,对于webpack压缩功能的优化很有意义。
    更多请看:详解CommonsChunkPlugin的配置和用法
    一般plugins配置如下:

      new webpack.optimize.CommonsChunkPlugin({
          name: 'vendors', // 将公共模块提取,生成名为`vendors`的chunk
          chunks: ['index','list','about'], //提取哪些模块共有的部分
          minChunks: 3 // 提取至少3个模块共有的部分
      }),
    

webpack 4 新增属性optimization,选项配置,原先的一些插件部分放到这里设置
optimization: {splictChunks:{}}
相当于之前的插件 CommonsChunkPlugin:详情参看下面的文章:Webpack 3.X - 4.X 升级记录

关于process.env.NODE_ENV

  1. 在node中,有全局变量process表示的是当前的node进程。process.env包含着关于系统环境的信息。但是process.env中并不存在NODE_ENV这个东西。NODE_ENV是用户一个自定义的变量,在webpack中它的用途是判断生产环境或开发环境的依据的。
  • 因此需要的是DefinePlugin这个插件:它允许我们创建全局变量,可以在编译时进行设置,因此我们可以使用该属性来设置全局变量来区分开发环境和正式环境。这就是 DefinePlugin的基本功能。同时结合package.json中设置:scripts中设置:
module.exports = {
  plugins: [
    // 设置环境变量信息
    new webpack.DefinePlugin({
      PRODUCTION: JSON.stringify(true),
      VERSION: JSON.stringify('5fa3b9'),
      BROWSER_SUPPORTS_HTML5: true,
      TWO: '1+1',
      'typeof window': JSON.stringify('object'),
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV)
      }
    })
  ]
}


"scripts": {
  "dev": "NODE_ENV=development webpack-dev-server --progress --colors --devtool cheap-module-eval-source-map --hot --inline",
  "build": "NODE_ENV=production webpack --progress --colors --devtool cheap-module-source-map",
  "build:dll": "webpack --config webpack.dll.config.js"
},

因此在项目打包中,为了区分开发环境和正式环境我们像如上配置即可,然后在webpack.config.js中通过 process.env.NODE_ENV 这个来区分正式环境还是开发环境即可。

References:

webpack vendors是干啥用的 多大算正常范围内 我的怎么320kb

详解CommonsChunkPlugin的配置和用法

彻底解决 webpack 打包文件体积过大

Webpack的dll功能使用

理解webpack之process.env.NODE_ENV详解(十八)

source-map

  1. 作用:
    提供从压缩好(打包好)的文件代码映射到源文件代码的功能,这样报错的时候可以直接调试源代码或者说直接定位到源代码的位置而不是定位到bundle***.js
  2. 配置
const webpackConfig = {
  mode: 'production',
  cache: false, // 开启缓存,增量编译
  // 默认为 false。设为 true 时如果发生错误,则不继续尝试,直接退出 bundling process
  bail: true,
  // cheap-module-eval-source-map is faster for development
  devtool: '#cheap-module-eval-source-map', // 生成 source-map 文件,开发环境
  devtool:'source-map',// 生产环境
  // ...
 }

关于devtool的一些可选项:
image

webpack-dev-server

是什么?

是webpack官方提供的一个小型express服务器,可以用来启动webpack,并接收webpack发出的文件变更信号,通过websocket协议自动刷新网页做到实时预览。

作用?

  1. 为静态文件提供功能
  2. 实时预览和HMR

如何做到实时预览?

  1. 通过dev-server启动的webpack会开启监听模式,当代码发生变化时会重新构建完通知dev-server。
  2. dev-server会让webpack构建出的js代码里面注入一个代理客户端。
  3. 网页和dev-server之间通过websocket协议通信,方便dev-server主动向客户端发送命令
  4. dev-server在收到来自webpack的文件变化时通过注入的客户端控制网页刷新。

HMR

  1. 直接替换掉更新的模块而不是重新加载
  2. 配置:
  devServer: {
    hot: true, //添加HMR
    port: 8090,
    host: '0.0.0.0',
    index: 'index.html',
    inline: true,
    overlay: true,
    compress: true,
    contentBase: path.join(__dirname, '../public'),
    historyApiFallback: true,
    disableHostCheck: true,

    proxy: {
      [`${apiPath}`]: {
        target: 'http://10.xx.xx.xx:8888',
        changeOrigin: true,
      },
    },
  },
  
  
  plugins:[
    new webpack.NamedModulesPlugin(),//显示模块的相对路径
    new webpack.HotModuleReplacementPlugin(), // 热部署替换模块
  ]

proxy

webpack-dev-server使用http-proxy-middleware把请求代理到一个外部的服务器。

proxy作用-代理远程接口
  1. 代理在很多情况下是必须使用的,针对一些静态文件可以通过本地服务器加载,但是请求数据肯定要用远程服务器了,此时则可以通过配置proxy解决。
  2. 解决开发环境跨域问题
配置
  • 针对问题1
 devServer: {
        historyApiFallback: true,
        hot: true,
        inline: true,
        contentBase: "./app",
        port: 8090,
        proxy: {
            "/api": {
              target: 'http://localhost:8888', //指定代理域名
              changeOrigin: true // 改变源到URL
            }
        },
        
    },
  1. 此时对于请求到/api/name的请求都会代理到http://localhost:8888/api/name
  2. 添加
// 主要用于重定向一个地址
  pathRewrite: {'^/api':''},

此时对于请求到/api/name的请求都会代理到http://localhost:8888/name
3. 默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。如果你想要接受,只要设置 secure: false 就行。修改配置如下:

module.exports = {
    //...
    devServer: {
        proxy: {
            '/api': {
                target: 'https://other-server.example.com',
                secure: false
            }
        }
    }
};
  1. 绕过处理
    Webpack-dev-server的proxy用法
// 待更新
  • 针对问题2
    主要通过
changeOrigin:true,
- 本地就会虚拟一个服务器接收你的请求并代你发送该请求.(终于明白了为啥原先vue里面axios发跨域请求的时候用到的是**反向代理**的意思)
- changeOrign顾名思义就是改变请求头中的origin字段,再结合官网(https://webpack.js.org/configuration/dev-server/#devserverproxy)的解释:The origin of the host header is kept when proxying by default, you can set changeOrigin to true to override this behaviour. It is useful in some cases like using name-based virtual hosted sites.
- 因此反向代理就很明显了:在浏览器接收到后端回复的时候,浏览器会以为这是本地请求,而在后端那边会以为是在站内的调用。

以下是vue-cli搭建的项目中配置接口地址代理示例:

module.exports = {
    dev: {
    // 静态资源文件夹
    assetsSubDirectory: 'static',
    // 发布路径
    assetsPublicPath: '/',

    // 代理配置表,在这里可以配置特定的请求代理到对应的API接口
    // 使用方法:https://vuejs-templates.github.io/webpack/proxy.html
    proxyTable: {
        // 例如将'localhost:8080/api/xxx'代理到'https://wangyaxing.cn/api/xxx'
        '/api': {
            target: 'https://test.cn', // 接口的域名
            secure: false,  // 如果是https接口,需要配置这个参数
            changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
        },
        // 例如将'localhost:8080/img/xxx'代理到'https://cdn.wangyaxing.cn/xxx'
        '/img': {
            target: 'https://cdn.test.cn', // 接口的域名
            secure: false,  // 如果是https接口,需要配置这个参数
            changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
            pathRewrite: {'^/img': ''}  // pathRewrite 来重写地址,将前缀 '/img' 转为 '/'。
        }
    },
    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 4200, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
}

再次感谢:Webpack-dev-server的proxy用法

webpack性能优化

减小文件搜索范围

  • resolve.extensions
  • resolve.modules
  • resolve.alias
  • include\exclude缩小文件匹配范围
resolve:{
    // 自动添加导入文件的后缀名
    extensions:['.js','.json'],
    // 配置webpack去哪些目录下寻找第三方模块
    // 默认值为node_modules会采用向上递归搜索的方式
    modules:[
        resolve('public')
        resolve('node_modules')
    ]// 配置别名,使用时直接用react就可以搜索到指定的文件
    alias:{
        'react':resolve('./node_modules/react/dist/react.min.js'),
        'assets':resolve('./public/assets')
    }},

mudule:{
    rules:{
        test:/\.js$/,
        // 缩小文件匹配范围,include包含哪些 exclude排除哪些
        include:[resolve('src')],
        exclude:/node_modules/,
        use:{
            loader:'babel-loader',
            options:{
                presets:["@babel/preset-env"]
            }
        }
    }
}
  • noParse
module:{
    // 忽略对大型库的解析能够提高webpack的构建性能
    // 注意忽略的这些内容不能有其他依赖引入(import require)
    // 即一般认为这类库不会依赖其他包的库,打包时没必要解析
    noParse:/node_modules\/(element-ui\.js)/,
}

构建动态链接库

dll:也就是上面讲过的动态链接库,包含大量复用模块(react react-dom)的动态链接库只需要构建一次,之后其中包含的模块都不需要重新编译直接拿来用就可以(只要不升级这些模块)
配置方法:

  • 新建一个专门配置文件:webpack.config.dll.js,这样就可以直接在package.json文件夹里面使用:npm run dll的时候构建动态链接库
"dll": "webpack --progress --colors --config ./build/webpack.config.dll.babel.js",

里面详细配置如下:DllPlugin

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');

module.exports = {
  // JS 执行入口文件
  entry: {
    // 把 React 相关模块的放到一个单独的动态链接库
    react: ['react', 'react-dom'],
    // 把项目需要所有的 polyfill 放到一个单独的动态链接库
    polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch'],
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
    // 也就是 entry 中配置的 react 和 polyfill
    filename: '[name].dll.js',
    // 输出的文件都放到 dist 目录下
    path: path.resolve(__dirname, 'dist'),
    // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
    // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
    library: '_dll_[name]',
  },
  plugins: [
    // 接入 DllPlugin
    new DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      // 例如 react.manifest.json 中就有 "name": "_dll_react"
      name: '_dll_[name]',
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, 'dist', '[name].manifest.json'),
    }),
  ],
};

构建输出的四个文件:绝对路径/dist/下
image
简单讲就是单独写配置文件:同样是entry output plugin配置

  • 如何使用这些dll呢
    使用的时候一般就是在正常配置文件里面了:webpack.config.dev.js主要配置如下:DllReferencePlugin
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

module.exports = {
  entry: {
    // 定义入口 Chunk
    main: './main.js'
  },
  output: {
    // 输出文件的名称
    filename: '[name].js',
    // 输出文件都放到 dist 目录下
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        // 项目源码使用了 ES6 和 JSX 语法,需要使用 babel-loader 转换
        test: /\.js$/,
        use: ['babel-loader'],
        exclude: path.resolve(__dirname, 'node_modules'),
      },
    ]
  },
  plugins: [
    // 告诉 Webpack 使用了哪些动态链接库
    new DllReferencePlugin({
      // 描述 react 动态链接库的文件内容
      manifest: require('./dist/react.manifest.json'),
    }),
    new DllReferencePlugin({
      // 描述 polyfill 动态链接库的文件内容
      manifest: require('./dist/polyfill.manifest.json'),
    }),
  ],
  devtool: 'source-map'
};

注:webpack.config.dll.js里面DllPlugin中的name参数必须和output.library一致。工作流程是:DllPlugin中的name参数会影响输入的mainfest.json文件中name字段的值,而在webpack.config.dev.js里面DllReferencePlugin会去mainfest.json中读取name字段的值,把该值作为从全局变量中获取动态链接库中内容时的全局变量名。

  • 其他:显然DllReferencePlugin里面需要引用动态链接库相关文件,所以一般项目里面
 npm install
 npm run dll
 npm start

使用HappyPack()

  • 为什么要用?
    因为loader在转换文件时转换操作通常是串行的,构建时间较长
  • 原理?
    就是把loader转换文件的操作变成并行的,减少构建时间
  • 没用过,具体配置以后再说

多进程压缩代码

parallelUglifyPlugin会开启多个子进程,把多个文件的压缩工作分配给多个子进程完成,但是每个子进程还是通过uglifyJS压缩代码,但是变成了并行。
配置如下:parallelUglifyPlugin,只是把原先使用uglifyJS替换为parallelUglifyPlugin:

const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = {
  plugins: [
    // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
    new ParallelUglifyPlugin({
      // 传递给 UglifyJS 的参数
      uglifyJS: {
        output: {
          // 最紧凑的输出
          beautify: false,
          // 删除所有的注释
          comments: false,
        },
        compress: {
          // 在UglifyJs删除没有用到的代码时不输出警告
          warnings: false,
          // 删除所有的 `console` 语句,可以兼容ie浏览器
          drop_console: true,
          // 内嵌定义了但是只用到一次的变量
          collapse_vars: true,
          // 提取出出现多次但是没有定义成变量去引用的静态值
          reduce_vars: true,
        }
      },
    }),
  ],
};
  • 另外:压缩代码会拖慢webpack编译速度,因此以上是在webpack.config.prod.js里面配置
  • 另外: 这次知道为啥代码里有控制台输出不用管的原因了吧!上线的时候这些console都会drop掉。

优化HMR

  • 原因:发生模块热替换时会在控制台看到这样的日志:image
    红线标明的地方表示ID为68的模块被替换了,显然开发者不知道68是什么鬼,但是利用NamedModulesPlugin可以把替换的模块名字显示出来:
const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin');

module.exports = {
  plugins: [
    // 显示出被替换模块的名称
    new NamedModulesPlugin(),
  ],
};

此时:
image

区分环境

区分开发环境和生产环境是一个很关键的问题:

if (process.env.NODE_ENV === 'production') {
  console.log('你正在线上环境');
} else {
  console.log('你正在使用开发环境');
}

具体配置如下:DefinePlugin

const DefinePlugin = require('webpack/lib/DefinePlugin');
module.exports = {
  plugins: [
    // webpack.config.prod.js
    new DefinePlugin({
      // 定义 NODE_ENV 环境变量为 production
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    // webpack.config.dev.js
    new webpack.DefinePlugin({
      'process.env.CLIENT_ENV': '"development"',
    }),
  ],
};
原理
  • 借助环境变量的值去判断使用哪一个分支

  • 其实是注入一个process模块模拟node里面的process,以支持使用process.env.CLIENT_ENV等这样的语句

    • 另外:注意在定义环境变量的值时用JSON.stringify包裹字符串的原因是环境变量的值需要是一个由双引号包裹的字符串,而JSON.stringify(‘production’)的值正好等于’“production”’。
    • 另外:DefinePLugin定义的环境变量只对webpack需要处理的代码有效,不会影响node运行时的环境变量的值。

babel-loader设置缓存

中文文档

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
          plugins: [require('@babel/plugin-transform-object-rest-spread')],
          // 下次构建时会尝试访问缓存,避免产生高性能消耗的babel重新编译
          cacheDirectory:true
        }
      }
    }
  ]
}

webpack tree-shaking

  • 目的:打包后的代码应该剔除那些用不到的代码
  • tips:
    为了把采用 ES6 模块化的代码交给 Webpack,需要配置 Babel 让其保留 ES6 模块化语句,修改.babelrc文件为如下:
{
  "presets": [
    [
      "env",
      {
        "modules": false
      }
    ]
  ]
}

其中"modules": false的含义是关闭 Babel 的模块转换功能,保留原本的 ES6 模块化语法。原因:
webpack 2.0 开始原生支持 ES Module,也就是说不需要 babel 把 ES Module 转换成曾经的 commonjs 模块了,想用上 Tree Shaking,请务必关闭 babel 默认的模块转义。

  • 配置:
    • 方法1:package.json scripts里面启动webpack的时候添加-optimise-minimize
    • 方法2:继续用上面压缩代码时用到的plugin:UglifyJSPlugin

webpack代码分离

webpack提取公共代码

webpack4之前都用commonsChunkPlugin,(主要目的还是提取那些公共模块出来,避免加载的时候时间过长,其实就是为vender单独打包做code-splitting)一般配置如下:

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

new CommonsChunkPlugin({
  // 从哪些 Chunk 中提取
  chunks: ['a', 'b'],
  // 提取出的公共部分形成一个新的 Chunk,这个新 Chunk 的名称
  name: 'common'
})

以上配置就能从网页 A 和网页 B 中抽离出公共部分,放到 common 中。

每个 CommonsChunkPlugin 实例都会生成一个新的 Chunk,这个新 Chunk 中包含了被提取出的代码,在使用过程中必须指定name属性,以告诉插件新生成的 Chunk 的名称。 其中chunks属性指明从哪些已有的 Chunk 中提取,如果不填该属性,则默认会从所有已知的 Chunk 中提取。
Chunk是一系列文件的集合,每个Chunk会包含这个Chunk的入口文件和入口文件依赖的文件。

webpack4之后开始这样使用:

module:{
    optimization:{
        splitChunks: {
          cacheGroups: {
            // 这里开始设置缓存的 chunks
            commons: {
              chunks: 'initial', // 必须三选一: "initial" | "all" | "async"(默认为异步)
              test: /[\\/]node_modules[\\/]/,
              name: 'vendors', // 要缓存的分隔出来的 chunk 名称
            },
          },
        },
        namedChunks: true, // 开启后给代码块赋予有意义的名称,而不是数字的 id
    }
}
webpack配置按需加载

按需加载实际上也是在做代码分割,

module.exports = {
  // JS 执行入口文件
  entry: {
    main: './main.js',
  },
  output: {
    // 为从 entry 中配置生成的 Chunk 配置输出文件的名称
    filename: '[name].js',
    // 为动态加载的 Chunk 配置输出文件的名称
    chunkFilename: '[name].js',
  }
};

// 文件里这样使用:使用import动态加载
window.document.getElementById('btn').addEventListener('click', function () {
  // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
  import(/* webpackChunkName: "show" */ './show').then((show) => {
    show('Webpack');
  })
});

注:
Webpack 内置了对import(*)语句的支持,当 Webpack 遇到了类似的语句时会这样处理:

以./show.js为入口新生成一个 Chunk;
当代码执行到import所在语句时才会去加载由 Chunk 对应生成的文件。
import返回一个 Promise,当文件加载成功时可以在 Promise 的then方法中获取到show.js导出的内容。

在使用import()分割代码后,你的浏览器并且要支持Promise API才能让代码正常运行, 因为import()返回一个 Promise,它依赖 Promise。对于不原生支持 Promise 的浏览器,你可以注入 Promise polyfill。

/* webpackChunkName: “show” /的含义是为动态生成的 Chunk 赋予一个名称,以方便我们追踪和调试代码。 如果不指定动态生成的 Chunk 的名称,默认名称将会是[id].js。/ webpackChunkName: “show” */是在 Webpack3 中引入的新特性,在 Webpack3 之前是无法为动态生成的 Chunk 赋予名称的。

  • 另外,在做vue项目的时候会发现文档里面提供了两种做按需加载的方式,一个是webpack提供的require.ensure方法,一个是es6的import函数(返回promise),两者均为动态加载。
    vue import 和 resolve => { }懒加载的区别

重点:讲一下react-router做按需加载,实际上react-loadable做代码分割也是这种原理,只不过它增加了错误状态的处理等等
同理,我们做图片的懒加载其实也是一样的道理比如vue-lazyload这个插件,往往会有加载成功的组件,会有error的组件即我们的默认logo图片

import React, {PureComponent, createElement} from 'react';
import {render} from 'react-dom';
import {HashRouter, Route, Link} from 'react-router-dom';
import PageHome from './pages/home';

/**
 * 异步加载组件,核心函数,一个高阶函数
 * @param load 组件加载函数,load 函数会返回一个 Promise,在文件加载完成时 resolve
 * @returns {AsyncComponent} 返回一个新组件用于封装需要异步加载的组件
 */
function getAsyncComponent(load) {
  return class AsyncComponent extends PureComponent {

    componentDidMount() {
      // 在高阶组件 DidMount 时才去执行网络加载步骤
      load().then(({default: component}) => {
        // 代码加载成功,获取到了代码导出的值,调用 setState 通知高阶组件重新渲染子组件
        this.setState({
          component,
        })
      });
    }

    render() {
      const {component} = this.state || {};
      // component 是 React.Component 类型,需要通过 React.createElement 生产一个组件实例
      return component ? createElement(component) : null;
    }
  }
}

// 根组件
function App() {
  return (
    <HashRouter>
      <div>
        <nav>
          <Link to='/'>Home</Link> | <Link to='/about'>About</Link> | <Link to='/login'>Login</Link>
        </nav>
        <hr/>
        <Route exact path='/' component={PageHome}/>
        <Route path='/about' component={getAsyncComponent(
          // 异步加载函数,异步地加载 PageAbout 组件
          () => import(/* webpackChunkName: 'page-about' */'./pages/about')
        )}
        />
        <Route path='/login' component={getAsyncComponent(
          // 异步加载函数,异步地加载 PageAbout 组件
          () => import(/* webpackChunkName: 'page-login' */'./pages/login')
        )}
        />
      </div>
    </HashRouter>
  )
}

// 渲染根组件
render(<App/>, window.document.getElementById('app'));

references:
react-loadable原理浅析
下面看一下react-loadable的源码
官网解释:A higher order component for loading components with dynamic imports.

  {
    path: `/test`,
    exact: true,
    component: Loadable({
      loader: () => import('../components/test'),
      loading: Loading,
    }),
  },

function createLoadableComponent(loadFn, options) {
  if (!options.loading) {
    throw new Error("react-loadable requires a `loading` component");
  }

  let opts = Object.assign(
    {
      loader: null,
      loading: null,
      delay: 200, //默认200ms
      timeout: null,
      render: render,
      webpack: null,
      modules: null
    },
    options
  );

  let res = null;

  function init() { // 单例模式
    if (!res) {
      res = loadFn(opts.loader);
    }
    return res.promise;
  }

  ALL_INITIALIZERS.push(init);

  if (typeof opts.webpack === "function") {
    READY_INITIALIZERS.push(() => {
      if (isWebpackReady(opts.webpack)) {
        return init();
      }
    });
  }

  return class LoadableComponent extends React.Component {
    constructor(props) {
      super(props);
      init();

      this.state = {
        error: res.error,
        pastDelay: false,
        timedOut: false,
        loading: res.loading,
        loaded: res.loaded
      };
    }

    static contextTypes = {
      loadable: PropTypes.shape({
        report: PropTypes.func.isRequired
      })
    };

    static preload() {
      return init();
    }

    componentWillMount() {
      this._mounted = true;
      this._loadModule();
    }

    _loadModule() {
      if (this.context.loadable && Array.isArray(opts.modules)) {
        opts.modules.forEach(moduleName => {
          this.context.loadable.report(moduleName);
        });
      }

      if (!res.loading) {
        return;
      }

      if (typeof opts.delay === "number") {
        if (opts.delay === 0) {
          this.setState({ pastDelay: true });
        } else {
          this._delay = setTimeout(() => {
            this.setState({ pastDelay: true });
          }, opts.delay);
        }
      }

      if (typeof opts.timeout === "number") {
        this._timeout = setTimeout(() => {
          this.setState({ timedOut: true });
        }, opts.timeout);
      }

      let update = () => {
        if (!this._mounted) {
          return;
        }

        this.setState({
          error: res.error,
          loaded: res.loaded,
          loading: res.loading
        });

        this._clearTimeouts();
      };

      res.promise
        .then(() => {
          update();
        })
        .catch(err => {
          update();
        });
    }

    componentWillUnmount() {
      this._mounted = false;
      this._clearTimeouts();
    }

    _clearTimeouts() {
      clearTimeout(this._delay);
      clearTimeout(this._timeout);
    }

    retry = () => {
      this.setState({ error: null, loading: true, timedOut: false });
      res = loadFn(opts.loader);
      this._loadModule();
    };
    // 高阶组件最终返回的还是组件
    render() {
      if (this.state.loading || this.state.error) {
        return React.createElement(opts.loading, {
          isLoading: this.state.loading,
          pastDelay: this.state.pastDelay,
          timedOut: this.state.timedOut,
          error: this.state.error,
          retry: this.retry
        });
      } else if (this.state.loaded) {
        return opts.render(this.state.loaded, this.props);
      } else {
        return null;
      }
    }
  };
}
function load(loader) {
  let promise = loader();

  let state = {
    loading: true,
    loaded: null,
    error: null
  };

  state.promise = promise
    .then(loaded => {
      state.loading = false;
      state.loaded = loaded;
      return loaded;
    })
    .catch(err => {
      state.loading = false;
      state.error = err;
      throw err;
    });

  return state;
}
function resolve(obj) {
  return obj && obj.__esModule ? obj.default : obj;
}

function render(loaded, props) {
  return React.createElement(resolve(loaded), props);
}
function Loadable(opts) {
  return createLoadableComponent(load, opts);
}

  • 另外:在我的react学习笔记系列(一)中已经讲过
  • React.createElement(type,[props],[…children]);参数列表
    • 字符串类型的参数,表示要创建的标签的名称。可以是component表示组件啊!
    • 对象类型的参数, 表示 创建的元素的属性节点,没有属性内容就写null即可
    • 子节点(包括其他虚拟DOM ,获取,文本子节点)
    • 参数n:其他子节点
  • 另外:bundle和chunk有什么区别呢?
    • bundle其实包含了很多个chunk,但是chunk又是独立的
    • 我们来看官网的解释:概念术语
    • 官网里面提到了 bundle splitting & code splitting ,目前我们总结的其实就是:splitChunks+按需加载

webpack配置 scope hoisting

是什么?

Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 “作用域提升”,是在 Webpack3 中新推出的功能。

原理

分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。 因此只有那些被引用了一次的模块才能被合并

注意: 由于 Scope Hoisting 需要分析出模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效。

配置
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');

module.exports = {
  resolve: {
    // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
    mainFields: ['jsnext:main', 'browser', 'main']
  },
  plugins: [
    // 开启 Scope Hoisting
    new ModuleConcatenationPlugin(),
  ],
};

references:ModuleConcatenationPlugin接入webpack

webpack配置cdn

CDN
是什么

CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。

简单问题
//cdn.com/id/app_a6976b6d.css 这样写的好处?

URL省略了前面的协议类型,这样做的好处是访问这些资源的适合会自动根据当前HTML的URL采用什么协议去决定用什么协议

CDN服务一般会给资源开启多长时间的缓存
  • 针对HTML文件:不开启缓存,HTML文件不会放在CDN服务上,放在自己的服务器上,同时关闭自己服务器的缓存,自己服务器只提供HTML文件和接口
  • 静态js css 图片,开启CDN和缓存放在CDN服务商去。同时每个文件名都是带有由文件内容生成的hash值,这样文件内容发生变化就会被重新下载不管缓存时间多长

如何配置CDN

下面讲一下webpack里面如何配置让把静态资源上传到CDN

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');

module.exports = {
  // 省略 entry 配置...
  output: {
    // 给输出的 JavaScript 文件名称加上 Hash 值
    filename: '[name]_[chunkhash:8].js',
    path: path.resolve(__dirname, './dist'),
    // 指定存放 JavaScript 文件的 CDN 目录 URL
    publicPath: '//js.cdn.com/id/',
  },
  module: {
    rules: [
      {
        // 增加对 CSS 文件的支持
        test: /\.css$/,
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          // 压缩 CSS 代码
          use: ['css-loader?minimize'],
          // 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
          publicPath: '//img.cdn.com/id/'
        }),
      },
      {
        // 增加对 PNG 文件的支持
        test: /\.png$/,
        // 给输出的 PNG 文件名称加上 Hash 值
        use: ['file-loader?name=[name]_[hash:8].[ext]'],
      },
      // 省略其它 Loader 配置...
    ]
  },
  plugins: [
    // 使用 WebPlugin 自动生成 HTML
    new WebPlugin({
      // HTML 模版文件所在的文件路径
      template: './template.html',
      // 输出的 HTML 的文件名称
      filename: 'index.html',
      // 指定存放 CSS 文件的 CDN 目录 URL
      stylePublicPath: '//css.cdn.com/id/',
    }),
    new ExtractTextPlugin({
      // 给输出的 CSS 文件名称加上 Hash 值
      filename: `[name]_[contenthash:8].css`,
    }),
    // 省略代码压缩插件配置...
  ],
};

关键点:

  • 静态资源的文件名需要带上文件内容算出来的hash值,避免被缓存如:
filename:'[name]_[chunkhash:8].js'
  • 不同类型的资源放到不同域名的CDN服务上去,防止资源并行加载被阻塞。
output.publicPath: '//js.cdn.com/id/',
css-loader.publicPath: '//img.cdn.com/id/'
WebPlugin.stylePublicPath: '//css.cdn.com/id/',

webpack 3.x-webpack 4

做过的项目中有的使用webpack 3 有的使用webpack 4.相对于3,4做出了不少改进,特此总结记录
references:Webpack 3.X - 4.X 升级记录
Webpack 4 配置最佳实践

webpack4的理念:零配置使用

开发环境和生产环境的区分

由mode属性分辨

第三方库build的选择
  • 在 Webpack 3 时代,我们需要在生产环境的的 Webpack 配置里给第三方库设置 alias,把这个库的路径设置为 production build 文件的路径。以此来引入生产版本的依赖。
  • 在 Webpack 4 引入了 mode 之后,对于部分依赖,我们可以不用配置 alias,比如 React。React 的入口文件是这样的:
'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

这样就实现了 0 配置自动选择生产 build。

但大部分的第三库并没有做这个入口的环境判断。所以这种情况下我们还是需要手动配置 alias。

Code Splitting

Webpack 4 下还有一个大改动,就是废弃了 CommonsChunkPlugin,引入了 optimization.splitChunks 这个选项。
optimization.splitChunks 默认是不用设置的。如果 mode 是 production,那 Webpack 4 就会开启 Code Splitting。
Webpack 4 抛弃了原有的 CommonChunksPlugin,换成了更为先进的 SplitChunksPlugin,用于提取公用代码。
它们的区别就在于,CommonChunksPlugin 会找到多数模块中都共有的东西,并且把它提取出来(common.js),也就意味着如果你加载了 common.js,那么里面可能会存在一些当前模块不需要的东西。
而 SplitChunksPlugin 采用了完全不同的 heuristics 方法,它会根据模块之间的依赖关系,自动打包出很多很多(而不是单个)通用模块,可以保证加载进来的代码一定是会被依赖到的。
下面是一个简单的例子,假设我们有 4 个 chunk,分别依赖了以下模块:
在这里插入图片描述
CommonsChunkPlugin提取后:
在这里插入图片描述
splitChunksPlugin提取后:
在这里插入图片描述

作者:腾讯IVWEB团队
链接:https://juejin.im/post/5b506ae0e51d45191a0d4ec9
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

tips

为什么通过webpack打包后的html文件不能直接访问?

references:

为何webpack打包后的文件要放在服务器上才能运行

webpack打包vue项目之后生成的dist文件该怎么启动运行

File协议

file协议与Http协议,HTTP请求与AJAX请求

浅谈FIle协议与Http协议及区别

vue通过build打包后 打开index.html页面是空白的

现象

之前在公司开发项目的时候,通常开发完毕冒烟用例通过后就扔给测试,由他们负责测试以及上线的操作。(整个过程是自动化的)。最近自己在实验室开发项目的时候发现一个问题,特此记录一下。

webpack打包完成后会有如下图1的提示,webpack打包生成的html文件,直接打开浏览器运行是白屏的如下图2。为什么会出现这种现象呢?

image
image

基本原因:

无法正常解析出js/css/img指向的路径,进行修改后即可,unix风格的文件路径解析方式是:’.‘表示当前目录,’…'表示上一级目录。

基本解决方式:

  • 修改js/css/img的引入路径即可解决直接本地打开网页显示白屏的问题。

  • 利用express小型服务器,放在服务器上运行即可。

根本原因:

http协议和file协议解析路径问题?

其实也不是上面的问题,就是路径不对。

根本解决方式:

assetsPublicPath:' / ' 默认为根目录,而index.html和static是在同一级目录下,因此,解决方法就是 assetsPublicPath:' ./ ' 斜杠前加一个点,表示同一级。

image

file协议:本地文件传输协议

什么是File

File协议主要用于访问本地计算机中的文件,就如同在Windows资源管理器中打开文件一样。

如何使用File

要使用File协议,基本的格式如下:file:///文件路径,比如要打开F盘flash文件夹中的1.swf文件,那么可以在资源管理器或浏览器地址栏中输入:file:///f:/flash/1.swf回车。

file协议的一些特点
  1. file协议只能在本地访问

  2. 本地搭建http服务器开放端口后他人也可以通过http访问到你电脑中的文件,但是file协议做不到

  3. file协议对应有一个类似http的远程访问,就是ftp协议,即文件传输协议。

  4. file协议无法实现跨域

uri中为什么本地文件file后面跟三个斜杠, http等协议跟两个斜杠?

因为URI结构是:
scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]

①协议:该URL的协议部分为“https”,标识网页使用的是https协议,在internet中可以使用多种协议(http,https,ftp等)

②域名:一个URL中也可以使用IP(119.75.217.109)作为域名,这个URL中域名为www.baidu.com

③端口:跟在域名后,以“ : ”(冒号)作为分隔符。如果省略端口,那么将采用默认端口。

④虚拟目录:虚拟目录不是必须部分。是从域名后第一个“/”开始到最后一个“/”为止。

⑤文件名:从域名后的最后一个“/”开始到“?”为止,是文件名部分,如果没有“?”,则是从域名后的最后一个“/”开始到“#”为止,是文件部分,如果没有“?”和“#”,那么从域名后的最后一个“/”开始到结束,都是文件名部分。文件名部分也不是一个URL必须的部分,如果省略该部分,则使用默认的文件名。

⑥锚:从“#”开始到最后都是锚,锚也不是一个URL必须的部分。

⑦参数:从“?”开始到“#”为止中间为参数,参数可以允许有多个参数,中间以“&”作为分隔符。

如果有host,前面是要加 // 的,因此对于 http 等这些网络地址来讲http://www.baidu.sb:80/ad/cas写成这样很自然。

如果是文件,文件没有 host ,所以中间的部分就不要了,就变成了file:///ad/cash

其实根据上面的定义来讲,下面的才是正确的。因为如果没有 host 的话,第一个 [] 的内容就不应该存在了啊。
file:/ad/cash
这种统一的写法也有个标准,叫CURIE。

其实最开始的那个 / 也是可以不要的呢,就看你是不是表示的是绝对地址了,一般来说都是用的绝对地址,如在公司内网中,其他服务器的文件地址是 file://host/path/file.ext
本机不用 host 部分,就直接 file:/// 即可。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值