前端百日进阶-6.自动化构建工具

131 阅读5分钟

什么是自动化构建工具

在平时代码开发中我们用到的语言工具相对于浏览器而言过于复杂无法识别,这时候就需要对我们写的代码进行一系列转换后在通过浏览器执行,这种转换的工具就叫自动化构建工具。当然自动化工具的优点不止如此,它还会帮助优化和压缩代码。

Gulp

因为我在平时很少用到gulp所以这里不做过多的介绍,下面是一个gulp结合平时常用的插件做的一个模板。需要了解可以参考官网和模板

// 实现这个项目的构建任务
const { series, parallel, src, dest, watch } = require('gulp')
// 安装sass可能会遇到python2没有的情况: npm install --global --production windows-build-tools
// 如果还不行: npm install --global node-gyp
const sass = require('gulp-sass')
const babel = require('gulp-babel')
const swig = require('gulp-swig')
const browserSync = require('browser-sync')
const imagemin = require('gulp-imagemin')
const del = require('del')
const loadPlugins = require('gulp-load-plugins')
// 这个plugins会包含很多glup的插件让plugins.xxx这样使用
const plugins = loadPlugins()
const bs = browserSync.create()

// 对打包html进行的配置
// 会去代替模板文件
const data = {
    // 在html这样使用{{ date }}
    date: new Date()
}

// 打包文件前先清理
const clean = () => {
    return del(['dist', 'temp'])
}

// 对scss文件进行编译成css,需要安装gulp-sass
const style = () => {
    return src('src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass())
        // 因为后面还要压缩所以打包到temp
        .pipe(dest('dist'))
        // 表示会热更新到服务器使用
        .pipe(bs.reload({ stream: true }))
}

// 对js文件进行打包
// 需要安装gulp-babel @babel/core @babel/preset-env
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
        .pipe(babel({ presets: ['@babel/preset-env'] }))
        // 因为后面还要压缩所以打包到temp
        .pipe(dest('temp'))
        .pipe(bs.reload({ stream: true }))
}

// 对html文件进行打包
// 需要安装gulp-swig
const page = () => {
    return src('src/*.html', { base: 'src' })
        .pipe(swig({
            data,
            // 防止模板缓存导致页面不能及时更新
            defaults: { cache: false }
        }))
        // 因为后面还要压缩所以打包到temp
        .pipe(dest('temp'))
        .pipe(bs.reload({ stream: true }))
}

// 打包图片文件和字体文件
// 需要安装gulp-imagemin
const image = () => {
    return src('src/assets/images/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('dist'))
}
const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}

// 服务器启动
const serve = () => {
    // 观察并热更新 
    watch('src/assets/styles/*.scss', style)
    watch('src/assets/scripts/*.js', script)
    watch('src/*.html', page)

    watch([
        'src/assets/images/**',
        'src/assets/fonts/**',
        'public/**'
    ], bs.reload)

    bs.init({
        notify: false,
        port: 2080,
        // open: false,
        // files: 'dist/**',
        server: {
            // 根据基本路径
            baseDir: ['temp', 'src', 'public'],
            routes: {
                // 在html使用/node_modules的时候会用node_modules替换
                '/node_modules': 'node_modules'
            }
        }
    })
}

// 这里主要是对打包过后的代码进行压缩
// 对应的plugins都需要安装gulp-xxx 驼峰要转换为下划线/:cleanCss <=> gulp-clean-css
const useref = () => {
    return src('temp/*.html', { base: 'temp' })
        // 这里表示对temp的文件进行压缩 
        .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
        // 根据情况进行压缩
        .pipe(plugins.if(/\.js$/, plugins.uglify()))
        .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
        .pipe(plugins.if(/\.html$/, plugins.htmlmin({
            // 对空白字符过滤
            collapseWhitespace: true,
            // 对css进行压缩
            minifyCSS: true,
            // 对js进行压缩
            minifyJS: true
        })))
        .pipe(dest('dist'))
}

// 输出public里面的静态文件
const extra = () => {
    return src('public/**', { base: 'public' })
        .pipe(dest('dist'))
}

// 不管是服务器启动还是打包都需要对这三个东西进行打包
const compile = parallel(style, script, page)

// series为同步等待执行, parallel为异步执行
const build = series(
    clean,
    parallel(
        series(compile, useref),
        image,
        font,
        extra
    )
)


const develop = series(compile, serve)

// 使用的时候可以gulp build 或者 gulp develop
module.exports = {
    build,
    develop
}

Webpack

Webpack是当前最为主流的一个打包工具。可以帮我们对文件进行js压缩、css压缩、编译模板文件等等,从而减少前端的工作量。

下面是一个webpack的模版,可以通过在vscode下载webpack插件进行自动创建。

const path = require('path');
module.exports = {
  // 开发模式: development 和 production 不同模式下会在不同场景进行不同的优化
  mode: 'development',
  // 入口文件
  entry: path.join(__dirname, 'src', 'index'),
  // 输出文件
  output: {
    path: path.join(__dirname, 'dist'),
    filename: "bundle.js",
  },
  // module包含很多loader
  // loader主要用于对文件做一个转换
  module: {
    rules: [{
      test: /.jsx?$/,
      include: [
        path.resolve(__dirname, 'src')
      ],
      exclude: [
        path.resolve(__dirname, 'node_modules')
      ],
      loader: 'babel-loader',
      query: {
        presets: [
          ["@babel/env", {
            "targets": {
              "browsers": "last 2 chrome versions"
            }
          }]
        ]
      }
    }, {
      loader: './my-loader'
    }]
  },
  // 默认引入文件后缀名
  resolve: {
    extensions: ['.json', '.js', '.jsx']
  },
  devtool: 'source-map',
  devServer: {
    contentBase: path.join(__dirname, '/dist/'),
    inline: true,
    host: 'localhost',
    port: 8080,
  }
};

Loader

webpack中loader主要作用是对输入文件做一个转换后在输出文件

const path = require("path");
module.exports = {
    mode: "development",
    entry: path.join(__dirname, "src", "index"),
    output: {
        path: path.join(__dirname, "dist"),
        filename: "bundle.js",
    },
    module: {
        rules: [
            {
                // 只匹配结尾为js的文件
                test: /.js$/,
                // 这里我们在当前文件夹创建一个自定义的loader
                loader: "./my-loader",
            },
        ],
    },
};

下面是我们my-loader的内容

module.exports = function(source) {
    // source为原文件内容
    console.log('run loader')
    // 最后返回修改过后的文件内容,这个内容必须是js文件内容
    return source;
};

可以看出我们可以对匹配的输入文件进行一系列的转换之后返回,这就是webpack中loader的原理。一般情况下我们不会自己去写loader而是使用现成的

const path = require("path");
module.exports = {
    mode: "development",
    entry: path.join(__dirname, "src", "index"),
    output: {
        path: path.join(__dirname, "dist"),
        filename: "bundle.js",
    },
    module: {
        // 这两个loader可以让webpack转换css文件
        // 使用前需要安装: npm install style-loader css-loader --save-dev
        rules: [
            {
                test: /\.css$/,
                use: ["style-loader", "css-loader"],
            },
        ],
    },
};

Plugin

webpack中插件主要用于在webpack执行生命周期的各个部分进行操作。下面是一个简单的plugin例子。

const path = require("path");

class MyPlugin {
  constructor (options) {
    console.log('my pulgin', options)
  }
  
  apply (compiler) {
    // 在这里面可以调用不同的钩子在不同时期执行代码
    // 钩子API: https://www.webpackjs.com/api/compiler-hooks/#done
    compiler.plugin('entryOption', compilation => {
      console.log('hhah')
    })
  }
}

module.exports = {
    mode: "development",
    entry: path.join(__dirname, "src", "index"),
    output: {
        path: path.join(__dirname, "dist"),
        filename: "bundle.js",
    },
    plugins: [new MyPlugin()]
};

一般情况下都会使用现成的plugin而不是自己写

SourceMap

由于打包过后的文件不容易阅读,所以在调试错误的时候会很麻烦。这时候就需要用到source map来给编译过后的文件定位到原来文件的位置。webpack中有7种不同的映射模式,具体了解可以前往webpack官网查看。一般情况下开发环境都可以使用cheap-module-eval-source-map,而生产环境则不需要source map

module.exports = {
    mode: "development",
    entry: path.join(__dirname, "src", "index"),
    devtool: 'cheap-module-eval-source-map',
};

DevServer

webpack-dev-server 可用于快速开发应用程序,不再需要每次改变代码后再次去编译

npm i webpack-dev-server --save

module.exports = {
    mode: "development",
    entry: path.join(__dirname, "src", "index"),
    // 之后启动可以npx webpack-dev-server
    devServer: {
        // 用于告诉服务器静态访问位置
        contentBase: "./public",
        // 开启热模块替换,在css改变时不需要页面刷新也可以改变样式
        // 若需要js也进行热模块替换,则需要在代码中写入逻辑
        hot: true,
        // 配置代理后可以跨域访问资源
        proxy: {
            "/api": {
                target: "https://api.github.com",
            },
        },
    },
};

如果要在js代码中也加入热模块替换

 if (module.hot) {
     module.hot.accept('module_url', function() {
         // 需要进行的逻辑
     })
  }

区分环境

安装内置合并文件

npm i webpack-merge --save

创建三个配置文件

公共配置: webpack.common.js

开发环境配置: webpack.dev.js

生产环境配置: webpack.prod.js

// webpack.common.js
const path = require("path");

module.exports = {
    entry: path.join(__dirname, "src", "index"),
};

// webpack.dev.js
const { merge } = require("webpack-merge");
const common = require("./webpack.common");

module.exports = merge(common, {
    mode: "development",
});

// webpack.prod.js
const { merge } = require("webpack-merge");
const common = require("./webpack.common");

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

之后根据不同的配置文件执行不同的代码逻辑 npx webpack --config webpack.prod.js

tree shaking

开启[tree shaking]((webpack.docschina.org/guides/tree…

const path = require("path");
module.exports = {
    mode: "development",
    entry: path.join(__dirname, "src", "index"),
    output: {
        path: path.join(__dirname, "dist"),
        filename: "bundle.js",
    },
    // 必须要使用es module才有效
    optimization: {
        // 使用后导出部分会调用内置导出函数
        usedExports: true,
        // 使用后只有用到的导出内容才会被打包,并且会进行变量之类的优化
        minimize: true,
        // 开启后直接会打包成一个函数
        // concatenateModules: true,
        // 不移除副作用函数
        // 需要同时在package.json中设置: "sideEffects": ["src/*.js"],
        // sideEffects: true,
    },
    devtool: 'source-map',
};

更多特性请参考官网或者这个网站

Rollup

相对于webpack这个打包工具,Rollup显得更加轻量没有webpack这么多的功能,只提供打包。所以在选型时,webpack更适合用于网站开发而Rollup更加适合内库开发。

下面是一个简单的例子

// rollup.config.js
import json from 'rollup-plugin-json';

export default {
  input: 'src/index.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  // 使用json插件把字符串转换成json
  plugins: [ json() ]
};

被打包文件

import * as config from '../package.json'
console.log(config.version)

打包后文件, 可以看出相对于webpack打包后的文件简单了很多

'use strict';

var version = "1.0.0";

console.log(version);