从零搭建一个 webpack 脚手架工具(二)

2019-12-16 11:38:10 浏览数 (1)

其他 loader 配置

配置完有关 CSS loader 后,还有一个问题,我们不想将 CSS 都插入到 style 标签中,如果 CSS 样式代码很多,会导致生成的 HTML 文件很大,我们希望使用 <link> 标签引入打包后的 CSS 文件(将 CSS 单独提取出来),这时候就要使用一个插件:mini-css-extract-plugin

下载:yarn add mini-css-extract-plugin -D

配置:

代码语言:javascript复制
// webpack.config.dev.js
let MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    // ....

    plugins: [
        new MiniCssExtractPlugin({
            // 抽离的样式叫什么名字(会生成在 css 文件夹下)
            filename: "css/main.css",
        });
    ],

    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    // 将 style-loader 替换掉(不再将 css 样式放在 style 标签中)
                    MiniCssExtractPlugin.loader,
                    'css-loader'
                ]
            }
        ]
    }
}

eslint

eslint 是 JS 语法的校验器,它提供了一个 loader:eslint-loader。使用之前需要先下载:yarn add eslint eslint-loader,配置如下:

代码语言:javascript复制
{
    rules: [
        {
            test: /.js$/,
            use: {
                laoder: 'eslint-loader',
            }
        }
    ]
}

设置好 loader 后,还要在项目根目录下建一个 .eslintrc.json 文件再进行其他配置。

当然,也可以来到这个网址 https://eslint.org/demo/[1],下载默认的配置文件。下载好后把文件修改成 .eslintrc.json 名称(名称前有一个点),然后把该文件剪切到项目根目录下。

需要注意的是,loader 的执行顺序是从右到左(对于一个规则,多个 loader 的情况,配置 .css laoder 时,use 项中有多个 loader)因此,less-loader 或者 sass-loader 先执行,让代码先转成原生的 CSS,然后使用 postcss-loader 优化 CSS 属性(比如添加属性后缀),然后是 css-loader 将 CSS 文件中 import 导入的文件添加进来,最后使用 style-loader 将 CSS 样式添加到 html 的 style 标签中;从下到上(对于一个多个规则,比如同是处理 .js 文件的配置,写了好几个规则(test)),因此,eslint-loader 应该放在所有 .js 规则中的最后一个(先检验,再做别的事情)。

代码语言:javascript复制
{
    rules: [
        {
            test: /.js$/,
            use: [
                loader: "babel-loader",
            ]
        },{
            test: /.js$/,
            use: [
                loader: "eslint-loader",
            ]
        }
    ]
}

也可以使用 options 中的 enforce 配置项:

代码语言:javascript复制
{
    rules: [
        {
            test: /.js$/,
            use: [
                loader: "eslint-loader",
                options: {
                    // 强制让这个 loader 最先执行
                    enforce: "pre"
                }
            ]
        },{
            test: /.js$/,
            use: [
                loader: "babel-loader",
            ]
        }
    ]
}

enforce 默认值是 normal,除了 prenormal 之外,还有 post,表示强制最后执行在 normal 之后执行这个 loader。

html-withimg-loader

当我们在 HTML 模板中有 img 标签时,img 标签的 src 的路径并不会被 webpack 转化,因此需要使用 html-withimg-loader,使用之前同样需要先下载。然后配置:

代码语言:javascript复制
{
    rules: [
        test: /.html/,
        use: 'html-withimg-loader'
    ]
}

暴露全局变量

在 webapck 中使用 jquery 时,可以这么引入:

代码语言:javascript复制
import $ from 'jquery';

但是这个 $ 变量并不在全局下(window)。如果我们想要将改变量暴露到全局中,需要使用 expose-loader

下载:yarn add expose-loader。将 jquery 模块暴露出来:

代码语言:javascript复制
import $ from "expose-loader?$!jquery";

?$! 中的 $ 就是指被暴露的变量名(expose-loader ? ! 是固定格式)。

当然,如果不想这么写,也可以在 rules 中进行配置:

代码语言:javascript复制
{
    rules: [
        test: require('jquery'),
        use: 'expose-loader?$'
    ]
}

配置好后,使用 jQuery 时,还需要进行引入:import $ from 'jquery'。如果不想每次都引入(或说不用引入),可以使用一个插件:provide-plugin。使用时不需要下载,webpack 自带,然后在 plugins 配置项中配置:

代码语言:javascript复制
{
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery'
        })
    ]
}

如果你在 HTML 中引入了第三方模块使用 script 标签,但在开发中如果再使用 import $ from 'jquery',webpack 就会多打包一次。为了不让 webpack 这样做,可以添加一个配置:

代码语言:javascript复制
module.exports = {
    plugins: [],
    // ...
    externals: {
        jquery: 'jQuery'
    }
}

使用 watch 简化操作

当代码一变化,就会自动打包。

代码语言:javascript复制
// webpack.config.dev.js

module.exports = {
    watch: true,    // 开启监听
    watchOptions: {
        poll: 1000, // 每秒打包一次
        // 防抖,一直输入代码,停止输入 500 毫秒后再打包。
        aggreateTimeout: 500,
        // 不需要进行监控的文件或目录
        ignored: /node_modules/
    }
}

webpack 小插件

1. cleanWebpackPlugin

该插件需要下载,功能是每次新的打包完成后,旧的打包目录会自动被删除。该插件需要传入一个参数,你要删除的路径,要删除多个目录可以传入一个数组。

2. copyWebpackPlugin

该插件需要下载。功能是将没有指定为入口的目录中的文件拷贝到打包后的目录中。 格式:

代码语言:javascript复制
new CopyWebpackPlugin([
    {from: '要拷贝的目录',to: '拷贝到哪里'}
]);

3. webpack.DefinePlugin

该插件是 webpack 自带的插件(不需要下载)。用它可以自定义环境变量。

代码语言:javascript复制
{
    plugin: [
        new webpack.DefinePlugin({
            // DEV 变量就是一个环境变量
            DEV: JSON.stringify('dev'),
            PRODUCTION: JSON.stringify('production')
        }),
    ]
}

一般不使用这种方式配置环境变量。

4. BannerPlugin

该插件是 webpack 自带的,有一个字符串参数,表示版权说明。

代码语言:javascript复制
{
    plugins: [
        new webpack.BannerPlugin("make 2019 by xxx"),
    ]
}

devServer 配置项

配置 devServer 之前需要先下载 webpack-dev-server: yarn add webpack-dev-server -D。 下载好之后,就可以在 webpack 配置项中去配置 webpack-dev-server 啦。

配置 devServer

devServer 的配置项很多,这里只对使用最多的做一下介绍。devserver 的配置应该是在开发环境下进行的。下面是一个简单的配置内容:

代码语言:javascript复制
if(isDev){      // 如果是开发环境
    config.devServer = {
        // 设置 host(默认就是 localhost)
        // 如果你希望服务器外部可访问
        // 可以这样指定:127.0.0.1
        // 即:通过 IP 地址的形式
        host: "localhost",
        // 设置端口号
        port: "8888",
        // 告诉服务器从哪个目录中提供内容
        // 默认它会查找 index.html 文件作为页面根路径展示
        contentBase: path.join(__dirname,"../build"),
        // 这个publicPath代表静态资源的路径(打包后的静态资源路径)
        publicPath: '/build/',
        // 当设置成 true时,任意的 404 响应都可能需要被替代为 index.html
        historyApiFallback: true,
        // 是否开启 模块热替换功能
        hot: true,
        // 是否让浏览器自动打开(默认是 false)
        open: true,
        // 被作为索引文件的文件名。
        // 默认是 index.html,可以通过这个来做更改
        index: 'demo.html',

        // 使用代理服务器
        proxy: {
            '/api': {
                // 当请求 /api 的路径时,就是用 target 代理服务器
                target: "http://loaclhost:3000",
                // 重写路径
                pathRewrite: {
                    '/api': ''
                }
            }
        }
    }
}

有时候我们不想使用代理,只是想单纯的模拟数据。就可以使用 webpack 给我们提供的一个 before 函数:

代码语言:javascript复制
{
    devServer: {
        // app 参数就是 express 框架的 express 实例
        before(app){
            app.get('/api',(req,res) => {
                // to do something...
            })
        }
    }
}

第三种方式,就是使用 webpack 的端口(服务端和 webpack(前端) 是一个端口)在服务端需要下载一个中间件:webpack-dev-middleware

代码语言:javascript复制
yarn add webpack-dev-middleware -D

然后服务端写入以下代码:

代码语言:javascript复制
const express = require("express");
const webpack = require("webpack");
const webpackMiddleware = require("webpack-dev-middleware");

// 引入写好的 webpack 配置文件
let config = require("./webpack.config.js");
let compiler = webpack(config);

// 绑定中间件
app.use(webpackMiddleware(compiler));

配置命令

来到 package.json 文件中,再添加一条命令,叫做 start,写下下面的内容:

代码语言:javascript复制
{
    "script": {
        "build": "cross-env NODE_ENV='development' webpack --config config/webpack.config.dev.js",
        "start": "cross-env NODE_ENV=development webpack-dev-server --config config/webpack.config.dev.js"
    }
}

然后运行 npm start 就会自动打开浏览器,跳转到我们指定的 localhost:8888 端口。

有一点需要注意,在开发环境不要设置 publicPath,因为 开发环境下 devServer 执行打包的内容是在内存里的,如果设置了 publicPath 保存后页面反而不会有刷新。应在生产环境再用 publicPath。还有一点就是,每次修改配置项都要重新运行命令,这是很费时的一件事,如何在更新配置文件后不用再次重启服务呢?这在下面会说到。

historyApiFallback 更具体的配置

通过传入一个对象,比如使用 rewrites 这个选项,可进一步地控制。

代码语言:javascript复制
{
    devServer: {
        historyApiFallback: {
            // 是个数组
            rewrites: [
                // b=表示 以 “/” 请求的页面,会返回 这个路径下的 html 文件
                { from: /^/$/, to: '/views/landing.html' },
                { from: /^/subpage/, to: '/views/subpage.html' },
                // 别的则会返回 404 页面
                { from: /./, to: '/views/404.html' }
            ]
        }
    }
}

devServer 中 publicPath 的配置

devServer 中的 publicPath 与 output 中的并不同。devServer 中的 publicPath 指的是 webpack-dev-server 的静态资源服务路径。假如我们打包的内容在 build 文件夹下,则 publicPath 应是 /build/,这里有个技巧,output 中指定的打包路径,比如:path: path.join(__dirname,'../build') 那么 devServer 的 publicPath 一般就是 join 方法中的那个 build。如果指定别的路径,很可能就会访问不到资源。

开启模块热替换功能

开启这个功能可以让我们修改文件并保持后,页面不会出现刷新的情况,页面中的内容是被动态更替了!这样减少了页面重新绘制的时间。在 devServer 中单纯的让 hot = true 是没有作用的,还需要一个 webpack 插件。这个插件是 webpack 内置的插件,不需要下载。具体配置步骤如下:

代码语言:javascript复制
/**
 * 来到 webpack 配置文件
 * 引入 热更替插件
*/
const webpack = require('webpack');
// 来到 devServer 选项
{
    devServer: {
        hot: true
    },
    // 添加 plugin
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
    ]
}

配置好 webpack 之后,还需要在入口程序处检测 module.hot 是否存在(这个对象是在 webpack 打包后自动加入的)。 假如我们的程序入口文件是 index.js,可以这么来写:

代码语言:javascript复制
// index.js
if(module.hot){
    // 调用 accept 方法开启热更替
    module.hot.accept();
}

上面步骤做完后,就可以使用 热更替了。如果有多个页面,则应为每个页面的入口作检验。

React 中使用热模块更替

在 React 中,index.js 常常做程序的入口,而 App.js 往往需要 index.js 的导入。module.hot.accept 方法可以接收两个参数,一个是路径字符串或者数组,另一个是回调函数。在 index.js 中可以这么来写:

代码语言:javascript复制
import React from 'react';
import ReactDOM from 'react-dom';

import App from './App.jsx';

function render(){
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    )
}

render();

if (module.hot) {
    console.log(module.hot);
    // 当第一个参数是数组时
    // 表示 有多个路径需要热模块更替
    // 回调用于在模块更新后触发的函数
    module.hot.accept('./App.jsx',() => {
        render();
    });
}

React 自己来提供了一个官方的热更替模块 —— react-hot-loader。使用它时需要下载: npm install react-hot-loader。使用时也需要配置。

  • 首先需要配置 webpack 文件:
代码语言:javascript复制
// 更改 entry:
{
    entry: ['react-hot-loader/patch', '../src/index.js'],
}
  • 然后来到 .babelrc 文件,添加一个 plugin:
代码语言:javascript复制
{
    "plugins": ["react-hot-loader/babel"]
}
  • 来到 index.js 文件处,你就可以直接把原来判断 module.hot 的内容给删掉了。而且 webpack 配置文件也不需要再引入 热更新插件(恢复没有热更新配置时的样子,但是 hot 项不要变成 false)。
  • 来到 App.js 文件,更改内容:
代码语言:javascript复制
import { hot } from 'react-hot-loader';
function App(){
    // ....
}

// 最后这样导出:
export default hot(module)(App);

还没完,还应该重新下载一个包:yarn add @hot-loader/react-dom 这个包和 react-dom 一样,只是它有热替换功能。下载之后,在 webpack resolve 配置项中写入:

代码语言:javascript复制
alias: {
    // 这样,你在引入 react-dom 时,就会引入这个包
    'react-dom': '@hot-loader/react-dom'
}

最后,重启服务,热更替模块就可以用了。使用 react-hot-loader 的好处就是,可以避免 React 组件的不必要渲染。

下一节将介绍 webpack 优化、代码分片与压缩,以及改造 create-react-app 的 webpack 配置,让其支持多页应用。

参考资料

[1]

eslint 配置文件下载: https://eslint.org/demo/

0 人点赞