模块化开发与webpack

1. 核心概念

webpack把一切静态资源视为模块,所以又叫做静态模块打包器。通过入口文件递归构建依赖图,借助不同的loader处理相应的文件源码,最终输出标环境可执行的代码。
通常我们使用其构建项目时,维护的是一份配置文件,如果把整个webpack功能视为一个复杂的函数,那么这份配置就是函数的参数,我们通过修改参数来控制输出的结果。
在开发环境中,为了提升开发效率和体验,我们希望源码的修改能实时且无刷新地反馈在浏览器中,这种技术就是HRM(Hot Module Replacement)。
借助于loader/plugin,我们可以差异化处理不同的文件类型。如果有个性化需求,还可以实现自定的loader/plugin。

2. 安装webpack

创建文件夹如 webpack,并在终端内执行

$ yarn init 一路回车
$ yarn add webpack webpack-cli -D

本节课安装的版本
webpack ^5.58.1
webpack-cli ^4.9.0
webpack从零到一
创建基础文件目录如下:

webpack/
src/
index.js
index.html
package.json

index.js

function test(content) {
  document.querySelector('#app').innerHTML = content;
}

test('something');


index.html


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>webpack</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
<script type="text/javascript" src="./output/main.js"></script>
// output/main.js是打包后的js代码路径
package.json

{
  "name": "webpack-study",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "test": "..."
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.58.1",
    "webpack-cli": "^4.9.0"
  }
}

补充打包命令,即对 ./src/index.js 文件进行编译,输出目录为output,指定开发环境参数和devtool类型可以不对产物进行压缩,便于后续的产物分析。

"scripts": {
  "build": "webpack ./src/index.js -o ./output --mode=development --devtool=cheap-module-source-map"
}

执行命令 yarn build,可见打包出的文件output/main.js 如下

这时候浏览器访问index.html 就可以看到something了!
webpack从一到二

  1. 新建webpack.config.js,将build命令执行的参数转移到该文件:
const path = require('path');

module.exports = {
  mode: 'development',
  devtool: 'cheap-module-source-map',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'output'),
    filename: 'main.js'
  },
}

// 修改package.json

"scripts": {
  "build": "webpack"
}

执行命令 yarn build,效果一模一样。

  1. 增加ES6的转换能力

创建 src/es6.js文件

export default class CountChange {
  count = 1
   
  increment = () => {
    this.count++
  }
  
  decrease = () => {
    this.count--;
  }
}
// index.js 中引入

import CountChange from './es6';

const instance = new CountChange();

function test(content) {
  document.querySelector('#app').innerHTML = content;
}

test(instance.count)

这时候打包产物(这是局部):

可见class CountChange还是老样子,虽然chrome已经支持了类的原生运行,但有些浏览器还是只能使用ES5的代码。再如我们使用装饰器的话,chrome也无能为力。

3.babel出场

·安装
$ yarn add @babel/core @babel/preset-env babel-loader -D

·增加配置

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

·再次打包,查看output/main.js

perfect!再接再厉,补上装饰器:

es6.js

const decorator = (target, key, descriptor) => {
  target[key] = function (...args) {
    console.log(this.count);
    return descriptor.value.apply(this, args);
  };
  return target[key];
}

export default class CountChange {
  count = 1

  @decorator
  increment() {
    this.count++;
  }
...
}

现在打包肯定是报错的,因为对于“@”这样的符号babel预设并不能直接识别。
·安装相应的插件
yarn add @babel/plugin-proposal-decorators -D
·配置

// webpack.config.js
  
{
  ...
  module: {
    rules: [{
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['@babel/preset-env']
          ],
          plugins: [
            ["@babel/plugin-proposal-decorators", { "legacy": true }],
          ]
        }
      }
  ...
}

·重新执行打包命令,得到main.js

注意,源码的target是类的原型对象,本示例中方法的装饰等于重写。

可用于生产的react脚手架
$ yarn add react react-dom @babel/preset-react -S

创建文件 src/react.js

   
import React from 'react';
import { render } from 'react-dom';

const App = () => <div>App</div>;

render(<App />, document.querySelector('#app'));


webpack.config.js

{
...
entry: './src/react.js', // 修改文件入口
// module.rules[0].use.options.presets
presets: [
   ...
   '@babel/preset-react'
],
}

打包查看HTML文件。

·缓存包提取


optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          filename: 'vendor.js',
          chunks: 'all',
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/
        },
      }
    }
  }

记得把产物引入 html文件。

· css loader

$ yarn add style-loader css-loader -D

·样式抽离

$ yarn add mini-css-extract-plugin -D

webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
...
plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
...
// module.rules
// MiniCssExtractPlugin.loader 代替 style-loader

别忘记我们一直使用的开发环境模式,所以没有压缩。

·自动引入脚本和样式表
虽然上面的样式抽离成功了,但是并没有出现在 HTML文件中,我们依然要像之前的 js 文件一样手动引入。对于动态生成的文件,这显然是不现实的。于是:

$ yarn add html-webpack-plugin -D

const HtmlWebpackPlugin = require('html-webpack-plugin')
...
plugins:[
    new HtmlWebpackPlugin()
],

效果是打包文件多了一个index.html:

这里的路径可以通过output 参数配置

问题是缺少 div#app 元素,所以我们使用模板:

plugins:[
    new HtmlWebpackPlugin({
	   template: './template.html'
    })
]

// template.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>webpack</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>


·HMR
到目前为止,我们一直在使用打包+刷新的模式查看代码效果,显然十分繁琐低效。借助本地开发服务器来解决这个问题:

$ yarn add webpack-cli webpack-dev-server -D

增加命令

“scripts”: {
“start”: “webpack serve”
}

启动效果:

这时候,我们修改App组件,可以看到页面已经实时更新了,只不过是【刷新】!继续:
webpack.config.js

{
devServer: {
port: 8000, // 顺便更改一下端口
hot: true
}
}

入口文件


render(, document.querySelector(’#app’));
if (module.hot) {
module.hot.accept(App, () => {
render(, document.querySelector(’#app’));
});
}
// 如果 App 组件是外部文件创建的,通常写作(与import导入的路径一致):
if (module.hot) {
module.hot.accept(’./App’, () => {
render(, document.querySelector(’#app’));
});
}

至此,一个功能相对完善的脚手架就搭建完成了!

异步组件
我们就下面的手动实现极简的异步(高阶)组件开始:

const lazy = fn => class extends React.Component {
  state = {
    Component: () => null
  }

  async componentDidMount() {
    const { default: Component } = await fn();
    this.setState({ Component });
  }
  
  render() {
    const Component = this.state.Component;
    return <Component {...this.props} />;
  }
}

const Async = lazy(() => import('./Async'));

// 指定产出的模块名称(注意这里的注释是有用的):
const Async = lazy(() => import(/* webpackChunkName: "Async" */ './Async'));

webpack 将以import 函数为分割点,import(’./Async’) 返回 promise,等待组件加载完成后,展示真正的组件。Component: () => null为默认内容。
在项目中使用了async/await,如果报错regeneratorRuntime is not defined,因为babel默认只转换新的JavaScript语法(syntax),如箭头函数等,而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,此时需要一些辅助函数(babel 6.x以下版本借助polyfill,需要在entry之前或根文件头部引入,本课程均以babel 7之后的标准讲解):

$ yarn add @babel/plugin-transform-runtime -D

.babelrc

plugins: [
“@babel/plugin-transform-runtime”,

]

思考runtime做什么的?

我们要知道,异步组件从本质上,解决的还是SPA用户体验的问题。它为webpack提供了代码分割的依据,使得使用率高或者加载时间长的组件代码独立出去,同时通过低成本的过渡交互,保证了网站的体验。

require.ensure

在此不久前,require.ensure也是非常流行的一个代码异步加载的方式。

require.ensure(
  dependencies: String[], // 依赖项
  callback: function(require), // 加载组件
  errorCallback: function(error), // 加载失败
  chunkName: String // 指定产出块名称
)
// eg
require.ensure([], function () {
    const ensure = require('./requireEnsure');
    ensure.default();
},
() => null, 'require-ensure')
// 最后两个参数均可缺省

打包可见生成了 output/require-ensu

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值