Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端工程化 / 构建自动化 #1

Open
SunshowerC opened this issue Nov 22, 2018 · 0 comments
Open

前端工程化 / 构建自动化 #1

SunshowerC opened this issue Nov 22, 2018 · 0 comments

Comments

@SunshowerC
Copy link
Owner

SunshowerC commented Nov 22, 2018

前端工程化

  前端工程化的概念在近些年来逐渐成为主流构建大型web应用不可或缺的一部分,在此我通过以下这三方面总结一下自己的理解。

  1. 为什么需要前端工程化。
  2. 前端工程化的演化。
  3. 怎么实现前端工程化。

为什么需要工程化

  随着近些年来前端技术的不断发展,越来越多复杂的业务放在了前端,前端不再是以前几个HTML + CSS + javascript就能解决的了。业务复杂了,需要维护的代码量就自然多了,如此一来,前端代码的可靠性,可维护性,可拓展性,以及前端web应用的性能,开发效率等等各方面就成了不得不考虑的问题。

  于是我们就产生了前端工程化这个概念,来解决这些问题。现阶段的前端工程化,需要考虑到各个方面,包括但不限于以下这几点:

提升开发效率

  • webpack-dev-server 热加载
      以前,我们的日常前端开发的流程是这样的: 修改代码 -> 切换IDE到浏览器 -> 刷新浏览器查看效果(有时候还需要清除缓存) -> 修改代码 ....。

      这套流程,尤其是刷新浏览器这个过程,无疑是相当低效繁琐枯燥的。 而webpack-dev-server 替我们解决了这个问题,它有两种模式,两种模式,一种是 watch 模式,功能是你修改代码,自动帮你刷新页面,无需手动刷新;另一种更加强大,基于 websocket 全双工通信技术,直接无刷新帮你把修改的代码替换掉。 从而极大程度上提高了开发效率。

  • 数据mock
      在后端接口还没提供的时候,前后端制定好共同的接口协议,开发时前端可以使用mock模拟数据,与后端彻底分离,并行开发。面向接口编程,尽可能减少前后端沟通成本。

优化性能

  • 代码合并压缩,混淆加密

  • 减少小图片请求
      webpack中url-loader:loader: 'url-loader?limit=8192',使得小于8kb的图片使用data:image base64 编码内联,减少图片请求量

  • 部署静态文件缓存管理
      使用webpack的内置的chunkhash功能,可以给生成的js文件添加hash后缀,标识文件版本。

提高代码质量

  • 模块化
      主要指 js 代码的模块化。以前的前端开发并没有模块化这个概念,这给维护大型项目带来了极大的困难。发展到现在的前端有很多模块化的方法可供选择,如seajs ,requirejs, webpack 等。 模块化能很大程度上提高了代码的可维护性

  • CSS 预处理
      通过sass,less 等css 预处理器,可以实现 css 文件的拆分,颗粒化,实现css可复用。而且通过autoprefixer或postcss 还可以让 css 样式对老旧浏览器向下兼容。

      此外,通过使用 css-modules 能够避免css全局污染的问题,极大提高css代码的可控性,不需要设定一堆命名空间与命名规范来限制。

  • ES6 + babel 编译
      javascript本身设计存在一定程度上的缺陷,例如“没有模块化”,“没有块级作用域”,“全局变量污染”,“回调地狱”等等之类的问题,为了改善这些缺陷,计算机协会在2015年推出了ECMAScript 6 标准(今年已经ES8 已经发布了),使用ES6的语法除了能有效减少代码量之外,还引入了块级作用域,模块化,类的语法糖,promise以及一些新的API,很大程度上填了以前javascript的遗留下的坑,以及提高了代码质量。

      不过即便过了两年,ES6也并没有被市面的主流浏览器完全支持,所以我们还需用 babel 将ES6 编译成ES5,再将一些不支持的API polyfill 处理。

  • eslint 代码检查
      一直一来,代码风格都是一场无休止的争论,每个人都有自己的代码风格习惯,而这些习惯无非就是tab还是空格,换不换行,加不加空格等等之类的琐事,与其通过制定规范去强行限制开发者的编写习惯,不如从工具层面彻底解决代码风格的问题。eslint可以自动处理一些代码风格的问题,直接将代码通过指定的规则格式化,使代码整体风格统一

      更进一步,eslint 还可以禁止代码的一些可能造成不良影响的行为(例如eval,未定义变量),使其抛出错误。降低代码产生bug的可能性。

  • 单元测试
      集成单元测试,提高代码可靠性。前端较为流行的单元测试 mocha,qunit 等

  • UI 自动化测试
      UI 自动化测试是 软件通过模拟浏览器,对页面进行UI操作,判断是否产生预想的UI效果。目前较为流行的UI自动化测试套件主要是 基于phantomjs的 nightmare

  • web组件化
      web组件化是通过自定义标签,从UI层面对代码的拆分,提高前端代码的可复用性。尽管w3c已经初步对web组件化制定了规范, 但目前浏览器对web 组件化的支持惨不忍睹,无法通过原生的方法来实现web组件,但目前流行的前端框架,如vue,angular,react都有提供自己的web组件化,从而提高代码可复用性

前端工程化的发展

<script> 直接引入加载

  在没有引入模块化的概念之前,前端往往需要手动处理js文件的依赖关系,例如;bootstartp 依赖 jquery,就需要在引入bootstrap之前引入jquery

<script src="src/jquery.min.js" ></script>
<script src="src/bootstrap.min.js" ></script>

  如果引入js文件顺序错了则会报错。 乍一看似乎没什么难度呀,是人都能分清是吧。那么请看下面这种情况:
  有 a.js, b.js, c.js, d.js, e.js 五个文件,其中

  • a 依赖 b和e,
  • b 依赖 d和e,
  • c 依赖 a和d,
  • d 依赖 e,
  • e 无依赖。

  那么根据以上关系,请按正确顺序引入js文件(黑人问号???)。当然,事实上也并不难区分其优先级,逐级递推就很快可以推断出引入顺序为 e,d,b,a,c

  毫无疑问,对于稍微复杂点的web工程,存在复杂依赖情况是极有可能发生的,并且把时间耗费在管理依赖关系上也不值当。

  所以就诞生了前端模块化

模块化标准(AMD,CMD,ES6 Module)

  经历了混乱加载的黑历史,我们终于迎来了js的模块化,忽如一夜春风来,一夜之间冒出一堆模块化标准。

  其中具有代表性的模块加载器分别是是遵循AMD(Asynchronous Module Definition)规范的RequireJS ,还有淘宝玉伯开源的 遵循CMD(Common Module Definition)规范的 SeaJS。 两者除了遵循规范不一样之外,封装模块有差别之外,都各有所长,而且对旧版本浏览器的支持都相当完美。

  当然除了这两个,还有各类其他开发者开发的模块加载器,当真是一番群魔乱舞百家争鸣的盛世呀。在此就不一一细述了。

  下面有请我们的主角出厂: ES6 Module

  ES6 Module 是新一代javascript标准 ECMAScript 6 的新增特性,其语法和Python相似,比较简洁易用。另外,相比于其他模块加载器,ES6 Module 是语法级别的实现,其静态代码分析相比于其他框架会更快更高效,方便做代码检测。

// import 基本语法
import React from 'react';    //等价于 var React = require("react");
import { stat, exists, readFile } from 'fs';
// 等价于 
// var fs = require('fs');
// var stat = fs.stat, exists = fs.exists, readFile = fs.readFile;

  
  而且,且不论其API优劣,其语法与前面说的模块化有什么区别的,ES6 Module最大优点是显而易见的: 它是官方标准,而不是其他妖艳贱货第三方开发的框架/库。跟着有名分的原配混,毫无疑问是有前途更稳定的吧

  当然,缺点也是很明显的,不同于RequireJS,SeaJS 向下兼容到极致(ie6+),ES6 Module 的兼容性还未覆盖绝大部分浏览器,支持ES6 Module的浏览器寥寥无几,虽然可以通过babel进行语法转译,不过兼容性毕竟是硬伤,唯有时间能治愈。

自动化构建工具(gulp,grunt)

  从描述可知,前端工程化需要做的事情,单凭人力一个一个去处理基本没有可能完成,那么,我们就需要学会使用工具,毕竟程序猿和猿之间最大的区别就是会不会使用工具

  grunt 和 gulp 就是自动化构建工具。我们通过安装对应的node_module,根据gulp/grunt 的API编写相对应的任务(如:css预处理,代码合并压缩,代码校验检查等任务,js代码转译),那么就可以生成我们想要的结果,完成前端工作流管理,极大程度地提高效率。其作用其实就相当于makefile 的make 操作,将手工操作自动化,其任务编写格式如下。

// gulp scss预处理任务
gulp.task('styles', function() {
  return gulp.src('src/styles/main.scss')
    .pipe(sass({ style: 'expanded' }))
    .pipe(autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
    .pipe(gulp.dest('dist/assets/css'))
    .pipe(rename({suffix: '.min'}))
    .pipe(minifycss())
    .pipe(gulp.dest('dist/assets/css'))
    .pipe(notify({ message: 'Styles task complete' }));
});

模块化打包器(webpack)

  前面说了那么多SeaJS,RequireJS的模块化 ,又有gulp ,grunt的自动化处理,想必都有点觉得这前端工程化的技术栈也太繁琐了吧。

  那么现在,你可以统统不用管啦,让我们推出终极解决方案:Webpack。  

  相比于seajs / requirejs 需要在浏览器引入 sea.js 、require.js 的模块解析器文件,浏览器才能识别其定义的模块。 webpack不需要在浏览器中加载解释器,而是直接在本地将模块化文件(无论是AMD,CMD规范还是ES6 Module)编译成浏览器可识别的js文件。

  另外,相对于gulp/grunt 的批处理工作流功能,webpack 也可以通过 loader、plugin的形式对所有文件进行处理,来实现类似的功能。

  其主要工作方式是: 整个项目存在一个或多个入口js文件,通过这个入口找到项目的所有依赖文件,通过loader,plugin进行处理后,打包生成对应的文件,输出到指定的output目录中。可以说是集模块化与工作流于一身的工具

clipboard.png


  当然,webpack也并非银弹。工具没有好坏,只有适合与否。即便是webpack也并非适用于所有场合。

  webpack 的最大特点是一切皆为模块,一切全包,最适和应用在SPA一站式应用场景。只有简单几个页面的情况下使用 webpack 反而可能会增加不必要的配置成本,反而直接用gulp或者其他工具处理代码压缩,css 预处理之类的工作会更加快捷易用。

  另外,除了最主流的 webpack 之外,同性质的模块化打包器还有 browserIfy,以及百度的 fis ,由于对这两者了解不多,就不一一比较了。

使用 webpack 实现工程化

  废话少说,talk is easy , show me the code,我们来看看webpack是怎么工作的。以下是一个配置了webpack-dev-server的本地开发webpack配置文件。 具体可访问 github 地址 查看完整信息

// webpack.dev.config.js
let path = require('path'),
      webpack = require('webpack');

let resolve = path.resolve;
let webRootDir = resolve(__dirname, '../');


module.exports = {
    entry: {                 // 入口文件,打包通过入口,找到所有依赖的模块,打包输出
        main: resolve(webRootDir, './src/main.js'), 
    },
    output: {
        path: resolve(webRootDir, './build'),  // 输出路径
        publicPath: '/build/',     // 公共资源路径
        filename: '[name].js'      // 输出文件名字,此处输出main.js, babel-polyfill.js ,  视情况可以配置[name].[chunkhash].js添加文件hash, 管理缓存
    },
    module: {
        rules: [   //模块化的loader,有对应的loader,该文件才能作为模块被webpack识别
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
            {
                test: /\.(png|jpg|gif|svg|ico)$/,
                loader: 'file-loader',
                options: {
                    name: '[name].[ext]?[hash]'
                }
            },
            {
                test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
                loader: 'file-loader'
            },
            {
                test: /\.css$/,
                loader: 'style-loader!css-loader'
            },
            {
                test: /\.scss$/,
                loader: 'style-loader!css-loader!autoprefixer-loader?{browsers:["last 5 version", "Firefox' +
                ' 15"]}!sass-loader?sourceMap&outputStyle=compressed'
            }
        ]
    },

    resolve: {
        extensions: ['.js'],  // 定义后缀名 ,import时可以省略“.js”后缀
        alias: {   // 别名。 如 import "./src/style/common.css"  ==> import "style/common.css"
            'components': resolve(webRootDir, './src/components'),
            'page': resolve(webRootDir, './src/page'),
            'style': resolve(webRootDir, './src/style'),
            'script': resolve(webRootDir, './src/script'),
            'static': resolve(webRootDir, './static')
        }
    },

    devServer: { // webpack-dev-server 热加载的配置
        host: '127.0.0.1',   //本地ip, 如需局域网内其他及其通过ip访问,配置"0.0.0.0"即可
        port: 8080,
        disableHostCheck: true,
        historyApiFallback: true,
        noInfo: true
    },

    performance: {
        hints: false
    },

}

module.exports.devtool = '#source-map'

/*插件*/
module.exports.plugins = (module.exports.plugins || []).concat([
    // webpack 变量定义,,可在其他模块访问到该变量值,以便根据不同环境来进行不同情况的打包操作。
    //  例如,在main.js 下 console.log( process.env.Node_ENV ) 输出 development字符串
    new webpack.DefinePlugin({
        'process.env': {     
            NODE_ENV: `"development"`
        }
    }),

])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant