什么是’前端工程化’

这里写图片描述

用工程化思想和工具来系统地解决前端开发过程中遇到通用问题,从而提升开发效率,增加代码的可扩展性和可维护性

‘前端工程化’ 解决什么问题

  1. 多人协作问题(规范,流程, 模块, 版本管理)
  2. 效率问题(自动化工具)
  3. 质量优化(单元测试,集成测试)
  4. 性能优化(合并部署工具)
  5. 安全性(转码,防御, xss 等等)

如何’前端工程化’

  1. 确定开发调试的项目目录结构
  2. 规划和部署上线的产品目录结构
  3. 根据开发环境开设计工作流
  4. 工具选择
  5. 撰写流程脚本
  6. 测试流程脚本
  7. 整理文档和其它工具

项目和产品发布的目录结构

这里写图片描述

建立开发环境与工作流

这里写图片描述

Step1: 构建开发环境

let proxyPort = 8081;

  return {
    entry: './www/static/js/app.js',
    output: {
      filename: `app.js`,
      path: path.resolve(__dirname, 'dist', 'js'),
      publicPath: '/js/',
    },
    module: {
      rules: [
        {test: /\.css$/, use: ['style-loader', 'css-loader']},
      ]
    },
    devServer: {
      proxy: {
        "*": `http://127.0.0.1:${proxyPort}`,
      }
    }
  };
}

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --quiet & http-server www -c-1 -p 8081"
  },

Step2 :构建部署脚本

#!/usr/bin/env node

const webpack = require('webpack');
const config  = require('../webpack.config.js');
const fs      = require('fs');
const path    = require('path');
const srcDir  = path.resolve(__dirname, '..', 'www');
const desDir  = path.resolve(__dirname, '..', 'dist');

console.log('building...')

webpack(config(), function(err){
  if(err) throw new Error(err);

  console.log('webpack done.');

  fs.readdir(srcDir, function(err,files){
    console.log('reading html files...')

    if(err) throw new Error(err);
    files.forEach(filename => {
      if(/\.html$/.test(filename)){
        let srcPath  = path.resolve(srcDir,filename);
        let desPath  = path.resolve(desDir,filename);

        let readStream = fs.createReadStream(srcPath);
        let writeStream = fs.createWriteStream(desPath);

        readStream.pipe(writeStream);
      }
    });

    console.log('done!');
  });
});

Step3: 文件处理和版本管理

module.exports = function(env = {}){

  const webpack     = require('webpack'),
        path        = require('path'),
        fs          = require('fs');
        
  let proxyPort = 8081,
      plugins   = [];

  if(env.production){
    //compress js in production environment
    plugins.push(
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
          drop_console: false,
         }
      })
    );
  }

  return {
    entry: {
      app: './www/static/js/app.js'
    },
    output: {
      filename: `[name]${env.production?'-[hash].min':''}.js`,
      path: path.resolve(__dirname, 'dist', 'js'),
      publicPath: '/js/',
    },
    resolve: {
      alias: {
        css: path.resolve(__dirname, 'www', 'static', 'css'),
        js:  path.resolve(__dirname, 'www', 'static', 'js')
      }
    },
    plugins,
    module: {
      rules: [
        {test: /\.css$/, use: ['style-loader', 'css-loader']},
      ]
    },
    devServer: {
      proxy: {
        "*": `http://127.0.0.1:${proxyPort}`,
      }
    }
  };
}


#!/usr/bin/env node

const webpack = require('webpack');
const config  = require('../webpack.config.js');
const fs      = require('fs');
const path    = require('path');
const srcDir  = path.resolve(__dirname, '..', 'www');
const desDir  = path.resolve(__dirname, '..', 'dist');
const through = require('through2');

console.log('building...');

webpack(config({production: true}), function(err, stats){
  if(err) throw new Error(err);

  console.log('webpack done.');

  let entries = stats.compilation.compiler.options.entry,
      hash = stats.hash;

  fs.readdir(srcDir, function(err,files){
    console.log('reading html files...')

    if(err) throw new Error(err);
    files.forEach(filename => {
      if(/\.html$/.test(filename)){
        let srcPath  = path.resolve(srcDir,filename);
        let desPath  = path.resolve(desDir,filename);

        let readStream = fs.createReadStream(srcPath);
        let writeStream = fs.createWriteStream(desPath);

        readStream.pipe(through.obj(function(content, encode, done){
          if(encode === 'buffer'){
            content = content.toString('utf8');
          }
          for(var entry in entries){
            content = content.replace(`/js/${entry}.js`, `/js/${entry}-${hash}.min.js`);
          }
          done(null, content, 'utf8');
        })).pipe(writeStream);
      }
    });

    console.log('done!');
  });
});

Step4 :处理css

module.exports = function(env = {}){

  const webpack     = require('webpack'),
        path        = require('path'),
        fs          = require('fs');

  const ExtractTextPlugin     = require('extract-text-webpack-plugin');
  
  function productionTag(){
    return env.production?'-[hash].min':'';
  }

  let proxyPort = 8081,
      plugins   = [
        new ExtractTextPlugin(`[name]${productionTag()}.css`)
      ];

  if(env.production){
    //compress js in production environment
    plugins.push(
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
          drop_console: false,
         }
      })
    );
  }

  return {
    entry: {
      app: './www/static/js/app.js'
    },
    output: {
      filename: `[name]${productionTag()}.js`,
      path: path.resolve(__dirname, 'dist', 'res'),
      publicPath: '/res/',
    },
    resolve: {
      alias: {
        css: path.resolve(__dirname, 'www', 'static', 'css'),
        js:  path.resolve(__dirname, 'www', 'static', 'js')
      }
    },
    plugins,
    module: {
      rules: [
        {test: /\.css$/, use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: [
            {
              loader: "css-loader",
              options: {
                importLoaders: 1
              }
            },
            {
              loader: "postcss-loader"
            }
          ]
        })},
      ]
    },
    devServer: {
      proxy: {
        "*": `http://127.0.0.1:${proxyPort}`,
      }
    }
  };
}

module.exports = {
  plugins: [
    require('postcss-smart-import')({ /* ...options */ }),
    require('precss')({ /* ...options */ }),
    require('autoprefixer')({ /* ...options */ }),
    require('postcss-clean')({ /* ...options */ })
  ]
}

Step5: 处理图片和HTML

module.exports = function(env = {}){

  const webpack     = require('webpack'),
        path        = require('path'),
        fs          = require('fs');

  const ExtractTextPlugin     = require('extract-text-webpack-plugin');
  
  function productionTag(){
    return env.production?'-[hash].min':'';
  }

  let proxyPort = 8081,
      plugins   = [
        new ExtractTextPlugin(`[name]${productionTag()}.css`)
      ];

  if(env.production){
    //compress js in production environment
    plugins.push(
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
          drop_console: false,
         }
      })
    );
  }

  return {
    entry: {
      app: './www/static/js/app.js'
    },
    output: {
      filename: `[name]${productionTag()}.js`,
      path: path.resolve(__dirname, 'dist', 'res'),
      publicPath: '/res/',
    },
    resolve: {
      alias: {
        css: path.resolve(__dirname, 'www', 'static', 'css'),
        js:  path.resolve(__dirname, 'www', 'static', 'js'),
        img: path.resolve(__dirname, 'www', 'static', 'image'),
        tpl: path.resolve(__dirname, 'www')
      }
    },
    plugins,
    module: {
      rules: [
        {test: /\.css$/, use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            {
              loader: 'css-loader',
              options: {
                importLoaders: 1
              }
            },
            {
              loader: 'postcss-loader'
            }
          ]
        })},
        { 
          test:/\.(png|jpg|jpeg)$/, 
          use: [
            {
              loader: 'url-loader',
              options: {
                limit: 8192
              }
            }
          ]
        },
        { 
          test:/\.(html)$/, 
          use: [
            {
              loader: 'file-loader',
              options: {
                name: '[name].[ext]'
              }
            },
            {
              loader: 'extract-loader'
            },
            {
              loader: 'html-loader',
              options: {
                minimize: true,
                ignoreCustomFragments: [/\{\{.*?}}/],
                attrs: ['img:src', 'link:href'],
                interpolate: true
              }
            },
          ]
        }
      ]
    },
    devServer: {
      proxy: {
        "*": `http://127.0.0.1:${proxyPort}`,
      }
    }
  };
}

#!/usr/bin/env node

const webpack = require('webpack');
const config  = require('../webpack.config.js');
const fs      = require('fs');
const path    = require('path');
const srcDir  = path.resolve(__dirname, '..', 'dist/res');
const desDir  = path.resolve(__dirname, '..', 'dist');
const through = require('through2');

console.log('building...');

webpack(config({production: true}), function(err, stats){
  if(err) throw new Error(err);

  console.log('webpack done.');

  let entries = stats.compilation.compiler.options.entry,
      hash = stats.hash;

  fs.readdir(srcDir, function(err,files){
    console.log('reading html files...')

    if(err) throw new Error(err);
    files.forEach(filename => {
      if(/\.html$/.test(filename)){
        let srcPath  = path.resolve(srcDir,filename);
        let desPath  = path.resolve(desDir,filename);

        let readStream = fs.createReadStream(srcPath);
        let writeStream = fs.createWriteStream(desPath);

        readStream.pipe(through.obj(function(content, encode, done){
          if(encode === 'buffer'){
            content = content.toString('utf8');
          }
          for(var entry in entries){
            content = content.replace(new RegExp(`/(${entry})\.`,'g'), `/$1-${hash}.min.`);
          }
          done(null, content, 'utf8');
        })).pipe(writeStream);
      }
    });

    console.log('done!');
  });
});

解决开发环境下的问题

// Identity loader
let loaderUtils = require("loader-utils");

let code = `
(function(){
var imgs = document.getElementsByTagName('img');
for(var i = 0; i < imgs.length; i++){
  var img = imgs[i];
  if(img.src.indexOf('require(') >= 0){
    var match = img.src.match(/require\\(['"](.*)?['"]\\)/);
    if(match){
      img.src = match[1].replace('img', '/static/image');
    }
  }
}
})();
`;

module.exports = function(source) {
  if(/require\(['"]tpl\/.*?['"]\)/.test(source)){
    source = code + source;
  }
  //console.log(source);
  return source;
};

Webpack的核心机制

这里写图片描述

资源发布到CDN

  1. 将图片等静态资源上传到CDN
  2. 替换css和js的路径
  3. 将css和js上传到CDN
  4. 替换html中的路径

资源发布到CDN

#!/usr/bin/env node

const webpack = require('webpack');
const config  = require('../webpack.config.js');
const fs      = require('fs');
const path    = require('path');
const srcDir  = path.resolve(__dirname, '..', 'dist/res');
const desDir  = path.resolve(__dirname, '..', 'dist');
const through = require('through2');

const CDN_domain = 'https://dn-h5jun.qbox.me';
let qiniu = require('node-qiniu');
qiniu.config({
  access_key: '_1TVDnFeo-hgXa5YwE36niFZk37IlkxglZfVCPQe',
  secret_key: 'o62Fkt9TzbUgywzcqImWaoWIZJJ66p6hE8mthscd'
});
let bucket = qiniu.bucket('h5jun');

console.log('building...');

webpack(config({production: true}), async function(err, stats){
  if(err) throw new Error(err);

  console.log('webpack done.');

  let entries = stats.compilation.compiler.options.entry,
      hash = stats.hash;

  //put file into CDN
  function putResToCDN(res){
    let srcPath  = path.resolve(srcDir,res);
    return new Promise((resolve, reject) => {
      bucket.putFile(hash + '/' + res, srcPath, function(err, reply){
        if(!err){
          var url = CDN_domain + '/' + hash + '/' + res; //reply.key;
          resolve([res, url]);
        }else{
          reject(err);
        }
      });  
    });
  }

  function getResFiles(){
    return new Promise((resolve, reject) => {
      fs.readdir(srcDir, function(err, files){
        if(err) reject(err);
        resolve(files);
      })
    })
  }

  let files = await getResFiles();
  let jscssFiles = files.filter(file => /\.js|\.css$/.test(file));
  let htmlFiles = files.filter(file => /\.html$/.test(file));
  let otherFiles = files.filter(file => !/\.js|\.css|\.html$/.test(file));

  console.log('uploading resources...');

  //先上传静态资源
  let resMap = await Promise.all(otherFiles.map(res => putResToCDN(res)));

  jscssFiles.forEach(filename => {
    let srcPath  = path.resolve(srcDir,filename);
   
    let content = fs.readFileSync(srcPath, 'utf8');
    //console.log(content);
    content = content.replace(/\/res\//mg, CDN_domain + '/' + hash + '/');

    fs.writeFileSync(srcPath, content);
  });

  let jscssMap = await Promise.all(jscssFiles.map(res => putResToCDN(res)));

  resMap = resMap.concat(jscssMap);

  console.log(resMap);

  console.log('replacing links...')

  htmlFiles.forEach(filename => {
    let srcPath  = path.resolve(srcDir,filename);

    if(/\.html$/.test(filename)){  
      let desPath  = path.resolve(desDir,filename);

      let readStream = fs.createReadStream(srcPath);
      let writeStream = fs.createWriteStream(desPath);

      readStream.pipe(through.obj(function(content, encode, done){
        if(encode === 'buffer'){
          content = content.toString('utf8');
        }
        
        for(var entry in entries){
          content = content.replace(new RegExp(`/(${entry})\.`,'g'), `/$1-${hash}.min.`);
        }
        
        content = content.replace(/\/res\//mg, CDN_domain + '/' + hash + '/');

        done(null, content, 'utf8');
      })).pipe(writeStream);
    }
  });

  console.log('done!');
});

其它工具

  1. grunt
  2. gulp
  3. baidu fis
  4. stc

总结

这一节我们学习了

  1. 前端工程化的概念以及这么做的原因
  2. 前端工程化的流程工具 npm script 和webpack
  3. 周边其它工具和脚本的撰写
    作业

为自己的小网站写工程化脚本
处理html, css, js和静态资源

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐