前端工程化与「Webpack」

作者: 果汁凉茶丶 | 来源:发表于2018-10-26 16:37 被阅读5次

    # 前端工程化

      近几年来,前端领域飞速发展,前端的工作早已不再是切几张图,写几个页面那么简单,项目比较大时,很可能会多人协同开发,模块化,组件化,CSS预编译等技术也被广泛的使用。前端自动化(半自动化)工程已经成为现在的主流。前端工程化主要解决一下问题

    • Javascript、CSS 代码的合并和压缩
    • CSS 预处理:Less,Sass, Stylus的编译
    • 生成雪碧图
    • ES6 转ES5 语法
    • 模块化
      ...

    # Gulp 与 Webpack

      小伙伴们应该都或多或少听说过Gulp和webpack,他们有什么区别呢?

      Gulp与Webpack都是前端打包工具,不过Gulp是流管理,合并压缩后的代码仍然是你写的代码,只是局部变量名被替换了,一些语法做了转换而已,整体的内容并没发生变化。

      而webpack打包后的代码已经不只是你写的代码,其中夹杂了很多webpack自身的模块处理代码。在编译过程中,webpack工程会自动载入一些内容。

    # Webpack 与工程

      Webapck 主要适用场景是 单页面富应用(SPA),SPA通常是由一个htnl文件,和一堆按需加载的js组成。webpack的依赖关系图如下所示

      如上图,左侧是业务中我们写的各种文件,包括js,less,jpg,这些格式的文件通过特定的加载器(Loader)编译之后,最终统一生成为右侧这些静态资源。

      在Webpack的世界里,一张图片,一个CSS甚至是一个字体,都被称为是一个模块,彼此存在依赖关系,webpack就是用来处理模块间的依赖关系,并把他们进行打包

    在传统的html中,如果我们需要引入一个css文件,通常在html页面的<head>部分使用<link> 将其引入

    <link rel="stylesheet" href="/css/index.css">
    

    而在webpack中,我们不需要在html中添加,而是直接在.js文件中使用。打包时,index.css会被打包进一个js文件里,通过动态创建<style>的形式来加载css样式,当然也可以进一步配置,在打包编译时把所有的css都提取出来,生成给一个css文件

    import 'src/css/index.css'  // ES 6
    require('src/css/index.css')
    

    | SPA 的一个html文件

      SPA是由一个html文件和一堆按需加载的js组成,而这个html结构可能会非常简单

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <title>webpack app</title>
      </head>
      <body>
        <div id="app"></div>
        <script type="text/javascript" src="/dist/main.js"><script>
      </body>
    </html>
    

      这就是整个SPA的html内容,理论上他可以实现譬如淘宝,知乎这样的大型项目,而其他的内容,都集中在了神奇的main.js这个文件中

    | ES6中的 exportimport

      在SPA项目里,一个模块就是一个JS文件,它拥有独立的作用域,为了使外部能读取里面定义的变量,需要将一个配置文件作为模块导出export。这个配置文件可以是对象、数组,常量、函数等。譬如有 config.js文件如下

    // 普通变量
    export var config = {
      version: '2.0'
    }
    // 函数
    export function  add(a, b) {
      return a + b
    }
    

    这些文件被导出之后,在需要使用的模块使用import导入,就可以在这个文件内使用这些模块了。譬如有main.js文件如下

    import { _, config, add } from './config.js'
    
    console.log(config)   // { version: '2.0' }
    console.log(add(1, 2))  // 3
    

      在导入中写到的模块名称都是在export文件中设置的,也就是说,用户必须要提前知道模块中的名称有些什么叫什么才能将其导出。有时候我们不想去了解名称叫什么,或者需要自定义名称,我们可以在模块导出时可以使用export default来实现

    // 也可以使用默认导出
    export default {
      version: '2.0'
    }
    

    导入时就可以使用自定义的名称了

    import conf from './config.js'
    

    我们还经常需要安装一些第三方库,在webpack中也可以直接导入。

    import Vue from ’vue‘
    import Vuex from 'vuex'
    import $ from 'jquery'
    

    # Webpack 的配置

      执行npm init并按回车跳过一些选项,可以得到一个package.json文件,
      使用NPM本地局部安装webpack: npm install webpack --save-dev
      接着安装 npm install webpack-dev-server --save-dev,安装完成后,可以看到在package.json文件中也多了这两个配置信息及版本号。

    | 添加启动脚本

      在 npm 配置文件 package.json 文件的script中添加一个快速启动 webpack-dev-server服务的脚本

    {
      "scripts": {
        "test": "echo \"Error: on test specified\" && exit 1 ",
        "dev": "webpack-dev-server --open --config build/webpack.config.js" 
      }
    }
    

      执行时运行 npm run dev 命令,就会执行脚本中dev字段的命令,其中,--open指的是运行命令后,自动打开浏览器,--config指读取配置文件的路径。webpack的项目中,webpack配置通常都命名为webpack.config.js, 并存放在build文件夹下。

      当然如果区分开发版本和正式版本,常区分命名webpack.dev.conf.jswebpack.prod.conf.js, 通常还会有一个webpack.base.conf.js用来配置两个版本相同配置,然后通过importmerge形式添加到各自版本中

      对于脚本中的配置,除了 --open 很多 --config之外,还有几个常用的一起在这里总结

    • --open: 在执行命令是自动在浏览器打开页面
    • --config: 指明读取的配置文件的路径
    • --progress: 在控制台打印编译过程信息
    • --host: 指明执行的IP地址,默认是 127.0.0.1,也就是 localhost
    • --port: 指明执行时启用的端口号
    • --hide-modules: 执行编译时不将webpack模块内容添加到编译输出文件中
      ......


    到此我们就绪了一切准备工作,可以开始配置Webpack了

    | 就是一个js文件而已

      关于webapck的配置,其实就是一个js文件:webpack.config.js 这里给出一个基础的配置案例(实际上往往我们会拆分成dev,prodbase三个配置文件),围绕案例我们来分析配置的几个要点:

    'use strict'
    const path = require('path')
    const webpack = require('webpack')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const merge = require('webpack-merge')
    const utils = require('./utils')
    
    var config = {
      // 入口
      entry: {
        app: './src/main.js'
      },
      // 出口
      output: {
        path: config.build.assetsRoot,
        filename: '[name].js',
        publicPath: process.env.NODE_ENV === 'production'
          ? config.build.assetsPublicPath
          : config.dev.assetsPublicPath
      },
      resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: {
          'vue$': 'vue/dist/vue.esm.js',
          '@': resolve('src'),      // @符号代表路径‘~/src’
          '@root': path.resolve(__dirname, './')
        }
      },
      // 加载器配置(需要加载器转化的模块类型)
      module: {
        rules: [
          {
            test: '/\.css$/',
            use: [ 'style-loader', 'css-loader' ]
          }
        ]
      }
      // 插件
      plugins: [
        new webpack.DefinePlugin({
          'process.env': require('../config/dev.env')
        }),
        new webpack.HotModuleReplacementPlugin(),
        new HtmlWebpackPlugin({
          filename: 'index.html',
          template: 'index.html',
          inject: true
        }),
      ]
    
    }
    
    module.exports = config
    

    1. 入口( Entry

    entry 的作用是告诉webpack 从哪里开始寻找依赖,并且编译。通常情况下,入口文件都是src文件夹下的main.js文件。也就是说webpack会从main.js开始工作。

    2. 出口( Output

    Output 是用来配置编译后的文件存储的路径和文件名。path用来存放打包后文件的输出目录,是必填项;filename用于指定编译后输出文件的名称;publicPath指定资源文件引用的目录,如你资源在CDN上,这里可以填CDN路径。这里需要提一下,案例中使用的代码片段解释如下。具体信息见第5点‘模式’

    如果当前‘模式’process.env.NODE_ENVproduction即生产模式,那么使用./config/index.jsbuild下的assetsPublicPath配置,否者使用dev下的配置。

    3. 加载器(Loaders)- webpack最重要的功能

      作为开箱即用的自带特性,webpack自身只支持javascript。而loader能够让webpack处理那些非javascript的文件, 并将他们转化成有效的模块,然后添加到模块图中供给程序使用。
      之前说过,webpack里每一个文件都是一个模块,不同后缀名的模块需要不同的加载器来处理。加载器并不是webapck本身就有的内容,需要使用什么加载器需要用户用使用npm来安装,如:
    npm install css-loader --save-dev
    npm install style-loader --save-dev   // style-loader用作于热更新功能

    在 webpack 配置中定义 loader 时,要定义在 module.rules 中,而不是 rules。为了使你受益于此,如果没有按照正确方式去做,webpack 会给出警告。

      在module对象的rule属性中可以指定一系列的loader加载器,每个加载器都必须配置两个特性

    • test 属性: 标识出应该被对应loader转换的是哪个文件或那种类型文件
    • use 属性: 表示转换是应该使用哪个loader,它的值可以是数组或字符串,如果是数组,它的编译顺序是从后往前

    案例中所表达的意思是:嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.css' 的路径」时,在你对它打包之前,先使用css-loader转换一下,在使用style-loader转换一下再打包。

      loader有三种使用方式

    (1)配置(建议): 在 webpack.config.js 文件中指定 module选项rules属性中指定loader
    (2)内联:在每个 import 语句中显式指定 loader。

    import Styles from 'style-loader!css-loader?modules!./styles.css';
    

    (3)CLI。在 shell 命令中指定它们。

    webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
    


     以一个vue项目为例,通常需要的Loader列表如下,其中涉及了vue文件的解析,ES6转ES5语法,图片资源,视频资源,文本字体等各种类型模块的处理

    module: {
        rules: [
          {
            test: /\.vue$/,
            loader: 'vue-loader',
            options: vueLoaderConfig
          },
          {
            test: /\.js$/,
            loader: 'babel-loader',
            include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
          },
          {
            test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: utils.assetsPath('img/[name].[hash:7].[ext]')
            }
          },
          {
            test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: utils.assetsPath('media/[name].[hash:7].[ext]')
            }
          },
          {
            test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
            }
          }
        ]
      },
    

    代码中频繁出现的utils.assetsPath的定义如下

    //默认返回的是~/dist/static/_path
    exports.assetsPath = function (_path) {
      const assetsSubDirectory = process.env.NODE_ENV === 'production'
        ? config.build.assetsSubDirectory
        : config.dev.assetsSubDirectory
    
      return path.posix.join(assetsSubDirectory, _path)
    }
    

    4. 插件(plugins

      loader 被用于转换某些类型的模块,而插件则用于执行范围更广的任务,包括:打包优化,资源管理和注入环境变量等等。

      使用插件前我们需要先用npm安装,然后用require将其引入,再把他添加到plugins数组中

    // 为你的应用程序生成一个html,然后自动注入生成的bundle
    const HtmlWebpackPlugin = require('html-webpack-plugin')   
     // 用于访问内置插件,如热更新
    const webpack = require('webpack') 
    // 用于合并其他配置文件到本文件中
    const merge = require('webpack-merge')  
    

      webpack的内置插件用法简单直接,案例中我们使用了它的热更新插件

    new webpack.HotModuleReplacementPlugin()
    

    除此之外,还有许多请参见webpack插件列表

    5. 模式 (mode

      通过将模式设置为 development,productionnone,可以启用对应环境下webpack内置的优化。默认production.

      在案例中,plugins选项提到了一个DefinedPlugin,代码片段为

    new webpack.DefinePlugin({
       'process.env': require('../config/dev.env')
    })
    

    其作用就是 设置开发环境变量为dev.env.js中配置的NODE_ENV,正因为采用了这种方案,在Output选项的publicPath属性设置时我们才能使用process.env.NODE_ENV软编码识别当前环境变量。

    DefinePlugin 是webpack的一个接口,它允许我们创建一个在编译时可以配置的全局变量,这对我们区分开发模式和发布模式的构建允许不同的行为非常有用,比如,在开发模式中记录日志,而在发布模式中不记录日志。本例子中我们只是使用其区分了环境变量

    dev.env.js代码文件如下:这里还mergeprod.env.js文件里的内容。经过配置后的模式(mode)的信息,其实就是这两个配置文件中的NODE_ENV变量

    'use strict'
    const merge = require('webpack-merge')
    const prodEnv = require('./prod.env')
    
    module.exports = merge(prodEnv, {
      NODE_ENV: '"development"',
      API_ROOT: '"http://localhost:3000"'
    })
    

    # Webpack的模块热替换 HMR

    为避免篇幅过长阅读疲劳,该模块内容请参见Webpack如何实现热发布一文

    结束语

      前端的大肆流行绝非偶然,正是这些工程化的思想引入,使得前端向大前端迈进成为可能,更加丰富的软编码思路,也使得前端适应性更强,灵活度更大,自动化(半自动化)的实现使得编码更加便捷,效率更高。做好前端,不能只做页面dog~

    相关文章

      网友评论

        本文标题:前端工程化与「Webpack」

        本文链接:https://www.haomeiwen.com/subject/bwbctqtx.html