Lenny-Hu / note

blog
5 stars 1 forks source link

gulp & webpack 工作流使用记录 #56

Open Lenny-Hu opened 5 years ago

Lenny-Hu commented 5 years ago

package.json

{
  "devDependencies": {
    "autoprefixer": "^9.6.0",
    "cssnano": "^4.1.10",
    "gulp": "^3.9.1",
    "gulp-postcss": "^8.0.0",
    "gulp-sass": "^4.0.2",
    "gulp-seajs-transport": "^0.4.0",
    "gulp-sftp": "^0.1.5",
    "gulp-uglify": "^3.0.2",
    "gulp-util": "^3.0.8",
    "gulp-watch": "^5.0.1",
    "minimist": "^1.2.0",
    "dayjs": "^1.8.16",
    "browser-sync": "^2.26.7",
    "node-sass": "^4.12.0"
  }
}

gulpfile.js

// 要求nodejs版本8.0+ gulp 3.9
const gulp = require('gulp');
const transport = require('gulp-seajs-transport');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
const postcss = require('gulp-postcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const sftp = require('gulp-sftp');
const minimist = require('minimist');
const watch = require('gulp-watch');
const path = require('path');
const gutil = require('gulp-util');
const fs = require('fs');
const dayjs = require('dayjs');
const browserSync = require('browser-sync').create();
const reload = browserSync.reload;

const argv = minimist(process.argv.slice(2));

const utils = {
  // 转换seajs,并没有合并
  seajs (globs, options = {}, dest) {
    return gulp.src(globs, options).pipe(transport())
      .pipe(gulp.dest(dest));
  },
  uglify (globs, options = {}, dest) {
    let uglifyOptions = {
      mangle: {
        reserved: ['define', 'require', 'exports', 'module', '$'] // 排除混淆关键字
      }
    };
    return gulp.src(globs, options.src || {}).pipe(uglify(options.uglify || uglifyOptions))
      .pipe(gulp.dest(dest));
  },
  sass (globs, options = {}, dest) {
    return gulp.src(globs, options)
      .pipe(sass({outputStyle: 'expanded'}).on('error', sass.logError)) // 输出标准格式,方便后处理
      .pipe(gulp.dest(dest));
  },
  css (globs, options = {}, dest) {
    return gulp.src(globs, options)
      .pipe(postcss([
        autoprefixer({overrideBrowserslist: ['cover 99.5%']}), // 覆盖率够狠
        cssnano()
      ]))
      .pipe(gulp.dest(dest));
  },
  styles (globs, dest, options) {
    return gulp.src(globs, options)
      .pipe(sass({outputStyle: 'expanded'}).on('error', sass.logError)) // 输出标准格式,方便后处理
      .pipe(postcss([
        autoprefixer({overrideBrowserslist: ['cover 99.5%']}), // 覆盖率够狠
        cssnano()
      ]))
      .pipe(gulp.dest(dest));
  },
  js (globs, dest, options = {}) {
    let uglifyOptions = {
      mangle: {
        reserved: ['define', 'require', 'exports', 'module', '$'] // 排除混淆关键字
      }
    };
    return gulp.src(globs, options.src || {}).pipe(transport())
      .pipe(uglify(options.uglify || uglifyOptions))
      .on('error', (err) => {
        gutil.log('uglify Error', err.message);
        this.end();
      })
      .pipe(gulp.dest(dest));
  },
  sftp (globs, options) {
    // https://www.npmjs.com/package/gulp-sftp
    let defOptions = {
      user: 'root',
      pass: '123456',
      callback () {
        console.log(`【上传完毕】${globs}`);
      }
    }
    let sftpOptions = { ...defOptions, ...options.sftp };
    // console.log('使用的参数', sftpOptions);
    return gulp.src(globs, options.src || {})
      .pipe(sftp(sftpOptions));
  }
};

const configs = {
  by_m: {
    host: '',
    base: './boya/dianping/mobile/',
    js (globs) {
      return utils.js(
        globs,
        `${configs.by_m.base}/dest/js/`,
        {
          src: {
            cwd: `${configs.by_m.base}/static/js/`,
            base: `${configs.by_m.base}/static/js/`
          }
        }
      );
    },
    styles (globs) {
      return utils.styles(
        globs,
        `${configs.by_m.base}/static/css/`,
        {
          base: `${configs.by_m.base}/static/scss/`
        }
      ).pipe(gulp.dest(`${configs.by_m.base}/dest/css/`)); // 同时拷贝到dest目录
    }
  },
  by: {
    host: '',
    base: './boya/dianping/www/',
    upload (globs) {
      return utils.sftp(globs, {
        sftp: {
          host: '10.21.168.128',
          remotePath: '/usr/local/code'
        },
        src: {
          cwd: './boya/',
          base: './boya/'
        }
      });
    },
    styles (globs) {
      return utils.styles(
        globs,
        `${configs.by.base}/css/`,
        {
          cwd: `${configs.by.base}/sass/`,
          base: `${configs.by.base}/sass/`
        }
      );
    },
    js (globs) {
      // 由于seajs base配置(开发环境base=www/js/2013,正式环境base=www/js),所以js/2013/目录下的文件要打包到了/dest目录下
      // 故 globs 只能是一个路径
      let _path = path.normalize(globs).includes(path.normalize('/2013/')) ? `${configs.by.base}/js/2013/` : `${configs.by.base}/js/`;
      return utils.js(
        globs,
        `${configs.by.base}/dest/`,
        {
          src: {
            cwd: _path,
            base: _path
          }
        }
      );
    }
  }
};

// ===mobile===
// 转换seajs模块到dest目录
gulp.task('m-js-seajs', function () {
  return utils.seajs(
    [
      '**/*.js',
      '!weixin/jquery.js',
      '!lib/**/*.js',
      '!common/base64.js',
      '!competition/main.js',
      '!competition/quiz.js'
    ], {
      cwd: `${configs.by_m.base}/static/js/`,
      base: `${configs.by_m.base}/static/js/` // 指定一个基准路径,使生成的模块ID都是相对于这个基准路径的
    },
    `${configs.by_m.base}/static/js/`
  );
});

// 压缩dest目录的js文件
gulp.task('m-js-min', ['m-js-seajs'], function () {
  return utils.uglify([
    '**/*.js',
    '!lib/**/*.js',
    '!weixin/jquery.js',
    '!**/*-debug.js'
  ], {
    src: {
      cwd: `${configs.by_m.base}/dest/js/`,
      base: `${configs.by_m.base}/dest/js/`
    }
  },
  `${configs.by_m.base}/dest/js/`
  );
});

gulp.task('m-js', ['m-js-seajs', 'm-js-min']); // 打包压缩移动端的JS文件
// ===mobile end====

// ===pc===
gulp.task('by-styles', function () {
  return configs.by.styles([
    '**/*.scss'
  ]);
});

gulp.task('by-sftp', function () {
  return configs.by.upload([
    'dianping/www/css/**/*',
    'dianping/www/js/**/*',
    'dianping/www/images/**/*'
  ]);
});
// ===pc end===

// 静态服务器
gulp.task('browser-sync', function () {
  let _config = configs[argv.watch];

  browserSync.init({
    proxy: _config.host,
    // server: {
    //   baseDir: _config.base
    // }
  });
});

// 监视文件修改并上传
gulp.task('default', function () {
  console.log(argv);
  // 修改版本号
  let filePath = path.resolve(__dirname, 'boya/dianping/application/configs/staticversion.php');
  let content = fs.readFileSync(filePath, {
    encoding: 'utf-8'
  });
  content = content.replace(/[0-9]{14}/g, dayjs().format('YYYYMMDDHHmmss'));
  fs.writeFileSync(filePath, content);
  console.log('修改静态资源版本号完成');

  switch (argv.watch) {
    // gulp --watch by
    case 'by':
      watch([
        `${configs.by.base}/sass/**/*.scss`,
        `${configs.by.base}/js/**/*.js`
      ], function (event) {
        if (event.type !== 'deleted') {
          let extname = path.extname(event.path);
          let map = {
            '.scss': 'styles',
            '.js': 'js'
          };
          configs.by[map[extname]](event.path);
        }
      });
      break;

    case 'by_m':
      // gulp --watch by_m
      watch([
        `${configs.by_m.base}/static/js/**/*.js`,
        `${configs.by_m.base}/static/scss/**/*.scss`
      ], function (event) {
        if (event.type !== 'deleted') {
          let extname = path.extname(event.path);
          let map = {
            '.scss': 'styles',
            '.js': 'js'
          };
          configs.by_m[map[extname]](event.path);
        }
      });
      break;
    default:
      break;
  }
  return watch([
    './boya/dianping/ajax/**/*.php',
    './boya/dianping/application/**/*.php',
    './boya/dianping/application/**/*.phtml',
    './boya/dianping/www/**/*',
    './boya/dianping/mobile/**/*'
  ], function (event) {
    if (event.type !== 'deleted') {
      configs.by.upload(event.path);
    }
  });
})
Lenny-Hu commented 5 years ago

Gulp && webpack等工具文章参考

Lenny-Hu commented 4 years ago

webpack 优化

Lenny-Hu commented 3 years ago

vue-cli4 + ant-design-vue 使用配置(部分)

该配置可以对momentjs、lodash、icon等打包进行优化,同时配有css精灵图。

vue.config.js

const path = require('path');
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin'); // ant ui的momentjs换成dayjs优化体积
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); // 优化lodash打包
const SpritesmithPlugin = require('webpack-spritesmith'); // css精灵图

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // js打包代码来源分析

const isProd = process.env.NODE_ENV === 'production';

module.exports = {
  // publicPath: isProd ? '/assets/frontend/' : '/',
  assetsDir: 'assets/frontend',
  devServer: {
    proxy: { // 代理api请求到接口
      '/api': {
        target: 'http://xxxx.com',
        ws: true,
        changeOrigin: true
      },
      '/assets': {
        target: 'http://xxx.com',
        ws: true,
        changeOrigin: true
      },
    }
  },
  pages: {
    index: { // 对于源代码不是在src目录结构这种情况可使用pages指定入口文件
      entry: ['./app/scripts/polyfill.js', './app/scripts/main.js'], // 多个文件作为一个入口,也可内部import
      chunks: ['polyfill', 'lib', 'ui', 'vendors', 'index'],  // 使用的代码块,对应cacheGroups
      chunksSortMode: 'manual' // 按照chunks数组顺序插入script标签
    }
  },
  css: {
    loaderOptions: {
      less: { // ant UI 主题配置
        javascriptEnabled: true,
        modifyVars: {
          'primary-color': '#1cc208',
          'link-color': '#0282e1',
          'border-color-base': '#e6e6e6',
          // 'border-radius-base': '8px'
        }
      }
    }
  },
  chainWebpack: (config) => { // 设置webpack的alias,方便内部引用包
    config.resolve.alias
      .set('vue$', 'vue/dist/vue.esm.js')
      .set('@', path.resolve(__dirname, 'app', 'scripts'))
      .set('@images', path.resolve(__dirname, 'app', 'images'))
      .set('@styles', path.resolve(__dirname, 'app', 'styles'))
      .set('@ant-design/icons/lib/dist$', path.resolve(__dirname, 'app/scripts/icons.js')); // 优化ant ui图标体积,仅引入常用图标,具体见js文件
  },
  configureWebpack: config => {
    config.optimization = { // 此处有一部分是vue-cli4的默认配置
      splitChunks: {
        chunks: 'all',
        minSize: 30000,
        minChunks: 1,
        maxAsyncRequests: 5,
        maxInitialRequests: 3,
        automaticNameDelimiter: '~',
        name: true,
        cacheGroups: {
          polyfill: { // 此处无效,具体要进一步查看
            name: 'polyfill',
            test: /[\\/]polyfill\./,
            priority: -5
          },
          lib: { // 打包vue基本依赖
            name: 'lib',
            test: /[\\/]node_modules[\\/](vue|vue-router|vue-meta|vuex|axios)[\\/]/,
            priority: -10
          },
          ui: {
            name: 'ui', 
            test: /[\\/]node_modules[\\/](dayjs|ant-design-vue|@ant-design)[\\/]/,
            priority: -10
          },
          vendors: { // 默认配置
            test: /[\\/]node_modules[\\/]/,
            priority: -20
          },
          default: { // 默认配置
            minChunks: 2,
            priority: -30,
            reuseExistingChunk: true
          }
        }
      }
    };

    config.plugins.push(
      new AntdDayjsWebpackPlugin({
        preset: 'antdv3'
      }),
      new LodashModuleReplacementPlugin(),
      new SpritesmithPlugin({
        src: { // 精灵图的小图标目录
          cwd: path.resolve(__dirname, 'app/images/icons'),
          glob: '*.png'
        },
        target: { // 生成精灵图和样式目录
          image: path.resolve(__dirname, 'app/styles/sprite/sprite.png'),
          css: path.resolve(__dirname, 'app/styles/sprite/_sprite.scss')
        },
        apiOptions: {
          cssImageRef: '../../../styles/sprite/sprite.png', // 生成的css中,使用的地址,该地址是相对于你是在何处import main.scss
          spritesheet_info: {
            name: 'icons-spritesheet' // 控制精灵图变量名称,运行查看_sprite.scss即可明白
          },
          generateSpriteName (filename) { // 控制精灵图的class名称,运行查看_sprite.scss即可明白
            let { name } = path.parse(filename);
            return `icons-${name}`;
          }
        },
        spritesmithOptions: {
          padding: 2 // 设置精灵图每个图标的之间的距离
        }
      })
    );

    if (isProd) {
      config.plugins.push(
        new BundleAnalyzerPlugin()
      );
    }
  }
};

banbel.config.js

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset',
    [
      '@babel/preset-env',
      { 
        modules: false,
        useBuiltIns: 'entry',
        corejs: { version: 3, proposals: true }
      }
    ]
  ],
  plugins: [
    ['@babel/plugin-transform-runtime', {
      corejs: 3
    }],
    ["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": true }] // `style: true` 会加载 less 文件
  ],
  env: {
    test: {
      presets: ['@babel/preset-env']
    }
  }
};

icons.js

/* 列出你自己用到的@ant-design/icons的图标 */

/* 下面是antd默认使用到的icon */
export { default as CloseOutline } from '@ant-design/icons/lib/outline/CloseOutline'
export { default as CheckOutline } from '@ant-design/icons/lib/outline/CheckOutline'
export { default as LoadingOutline } from '@ant-design/icons/lib/outline/LoadingOutline'
export { default as CheckCircleOutline } from '@ant-design/icons/lib/outline/CheckCircleOutline'
export { default as InfoCircleOutline } from '@ant-design/icons/lib/outline/InfoCircleOutline'
export { default as CloseCircleOutline } from '@ant-design/icons/lib/outline/CloseCircleOutline'
export { default as ExclamationCircleOutline } from '@ant-design/icons/lib/outline/ExclamationCircleOutline'
export { default as CheckCircleFill } from '@ant-design/icons/lib/fill/CheckCircleFill'
export { default as InfoCircleFill } from '@ant-design/icons/lib/fill/InfoCircleFill'
export { default as CloseCircleFill } from '@ant-design/icons/lib/fill/CloseCircleFill'
export { default as ExclamationCircleFill } from '@ant-design/icons/lib/fill/ExclamationCircleFill'
export { default as UpOutline } from '@ant-design/icons/lib/outline/UpOutline'
export { default as DownOutline } from '@ant-design/icons/lib/outline/DownOutline'
export { default as LeftOutline } from '@ant-design/icons/lib/outline/LeftOutline'
export { default as RightOutline } from '@ant-design/icons/lib/outline/RightOutline'
export { default as RedoOutline } from '@ant-design/icons/lib/outline/RedoOutline'
export { default as CalendarOutline } from '@ant-design/icons/lib/outline/CalendarOutline'
export { default as SearchOutline } from '@ant-design/icons/lib/outline/SearchOutline'
export { default as BarsOutline } from '@ant-design/icons/lib/outline/BarsOutline'
export { default as StarFill } from '@ant-design/icons/lib/fill/StarFill'
export { default as FilterOutline } from '@ant-design/icons/lib/outline/FilterOutline'
export { default as CaretUpOutline } from '@ant-design/icons/lib/outline/CaretUpOutline'
export { default as CaretDownOutline } from '@ant-design/icons/lib/outline/CaretDownOutline'
export { default as PlusOutline } from '@ant-design/icons/lib/outline/PlusOutline'
export { default as FileOutline } from '@ant-design/icons/lib/outline/FileOutline'
export { default as FolderOpenOutline } from '@ant-design/icons/lib/outline/FolderOpenOutline'
export { default as FolderOutline } from '@ant-design/icons/lib/outline/FolderOutline'
export { default as PaperClipOutline } from '@ant-design/icons/lib/outline/PaperClipOutline'
export { default as PictureOutline } from '@ant-design/icons/lib/outline/PictureOutline'
export { default as EyeOutline } from '@ant-design/icons/lib/outline/EyeOutline'
export { default as DeleteOutline } from '@ant-design/icons/lib/outline/DeleteOutline'
export { default as UploadOutline } from '@ant-design/icons/lib/outline/UploadOutline'

polyfill.js

// 主要配置一些IE不支持而用到的特性
// ie
import 'core-js/features/promise';
import 'core-js/features/set';
import 'core-js/features/symbol';
import 'core-js/features/array/find';
import 'core-js/features/array/includes';
import 'core-js/features/array/map';

// HTMLElement.dataset
// Needed for: IE10-
(() => {
  if (!('dataset' in document.createElement('span')) &&
      'Element' in global && Element.prototype && Object.defineProperty) {
    Object.defineProperty(Element.prototype, 'dataset', { get: function() {
      var result = Object.create(null);
      for (var i = 0; i < this.attributes.length; ++i) {
        var attr = this.attributes[i];
        if (attr.specified && attr.name.substring(0, 5) === 'data-') {
          (function(element, name) {
            var prop = name.replace(/-([a-z])/g, function(m, p) {
              return p.toUpperCase();
            });
            result[prop] = element.getAttribute('data-' + name); // Read-only, for IE8-
            Object.defineProperty(result, prop, {
              get: function() {
                return element.getAttribute('data-' + name);
              },
              set: function(value) {
                element.setAttribute('data-' + name, value);
              }});
          }(this, attr.name.substring(5)));
        }
      }
      return result;
    }});
  }
})();

main.js

// import './polyfill';

import Vue from 'vue';
import {
  ConfigProvider,
  Empty,
  Button,
  Carousel,
  Modal,
  FormModel,
  Input,
  Skeleton,
  Spin,
  message,
  Dropdown,
  Icon,
  Menu,
  Breadcrumb
} from 'ant-design-vue';

import App from '@/views/layouts/app';
import router from '@/routes';
import store from '@/store';
import $http from '@/plugins/$http';
import $bus from '@/plugins/$bus';

import './helpers/filter';
// import Antd from 'ant-design-vue';
// import 'ant-design-vue/dist/antd.css';
// import 'ant-design-vue/dist/antd.less';
// Vue.use(Antd);

if (process.env.NODE_ENV === 'development') {
  console.log('开发环境,加载miragejs');
  // require('../apis'); // NOTE: just for dev
}

Vue.config.productionTip = false;
Vue.use(ConfigProvider);
Vue.use(Empty);
Vue.use(Button);
Vue.use(Carousel);
Vue.use(Modal);
Vue.use(FormModel);
Vue.use(Input);
Vue.use(Skeleton);
Vue.use(Spin);
Vue.use(Dropdown);
Vue.use(Icon);
Vue.use(Menu);
Vue.use(Breadcrumb);
Vue.prototype.$message = message;
Vue.prototype.$info = Modal.info;
Vue.prototype.$success = Modal.success;
Vue.prototype.$error = Modal.error;
Vue.prototype.$warning = Modal.warning;
Vue.prototype.$confirm = Modal.confirm;
Vue.prototype.$destroyAll = Modal.destroyAll;

Vue.use($http);
Vue.use($bus);

new Vue({
  el: '#app',
  components: { App },
  template: '<app/>',
  router,
  store
});