前端工程:從搭建一個webpack多頁應用開始

​時下,前端領域Vue/React/Angular三劍客橫行,一般來說,開發前端項目都會從這三個框架中選取一個,一些大廠甚至會基於這三大框架構建更適合業務的開發框架。

然而有些時候,需要開發一些比較小的項目,只有一個頁面或者只有幾個小頁面,這時候用這些框架就顯得有點重了,直接原生開發又無法使用sass/ES6之類的新技術,總歸是有些不舒服的。這種情況下,可以搭建一個小型的webpack多頁面項目。

接下來一步步從零搭建一個webpack+typescript+babel+sass的多頁應用腳手架。

一、配置webpack

現在webpack基本上已經成爲前端工程項目的標配,不僅平時工作中需要使用到,也是前端面試必不可少的一環,曾經一度還有webpack配置工程師的說法。

先初始化一個項目吧:

npm init -y

再添加一下webpack和webpack-cli

npm i webpack webpack-cli -D

建立一個webpack配置文件 webpack.config.js:

const config = {
  mode: 'development',
  entry: {},
  output: {},
  resolve: {},
  module: {},
  plugins: [],
};
​
module.exports = config;

添加入口與輸出位置:

{
  entry: {
    index: './src/pages/index.ts',
    about: './src/pages/about.ts',
  },
  output: {
    filename: 'assets/js/[name].[hash:8].js',
    path: './dist/',
  },
}

不想多做處理,可以就直接寫死入口,每多一個頁面,就在配置中多加一個入口,這樣的話,一兩個頁面沒什麼問題,但是問題稍微多一點,就不太好了,作爲一個工程師,肯定是需要花半個小時做成自動化的來節省每次幾十秒的手動配置的啦。

二、自動讀取入口文件

想要自動讀取也很簡單,只需要定好創建入口文件規則,然後使用Node Path API遍歷一下,自動添加到webpack配置文件中就好了。主要用到path和fs兩個包,那麼寫兩個工具函數來讀取一下入口文件夾吧。

const path = require('path');
const fs = require('fs');
​
​
/**
 * 同步判斷文件是否存在
 * 
 * @param {string} file 文件地址
 */
function isFileExistAsync(file) {
  try {
    fs.accessSync(file, fs.constants.F_OK);
  } catch (err) {
    return false;
  }
  return true;
}
​
​
/**
 * 遍歷某個文件夾,找出該文件夾下的所有一級子文件夾
 * 
 * @param {string} dir 文件目錄地址
 */
function readDir(dir) {
  let res = [];
  const list = fs.readdirSync(dir);
  list.forEach(file => {
    const pageDir = path.resolve(dir, file);
    const info = fs.statSync(pageDir);  
    if(info.isDirectory()){
      res.push(pageDir);
    }
  });
  return res;
}

有了這兩個函數,咱們可以把符合要求的入口文件添加的config對象中,暴露給webpack。

const entryScriptExt = 'ts';// 入口文件是ts文件,也可以換成js文件
readDir(pageRoot).forEach(dir => {
  const entry = path.resolve(dir, `index.${entryScriptExt}`);
  if (isFileExistAsync(entry)) {
    const { name } = path.parse(dir);
    config.entry[name] = entry;
  }
});

三、Babel處理

目前web端還不支持ES6/7,新一點瀏覽器支持性會好一些,但也不是完全支持,想要使用ES6/7以及TypeScript還是需要使用Babel將代碼編譯到es5才行,另外這裏使用TypeScript,Babel7中可以使用@babel/preset-typescript來編譯TypeScript,這樣也就不需要再加載一個ts-loader了。

先添加依賴:

npm i @babel/core @babel/preset-env @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread babel-loader -D

修改webpack配置:

{
  module: {
    rules: [{
      test: /\.(ts|js)?$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            '@babel/preset-env',
            '@babel/preset-typescript',
          ],
          plugins: [
            '@babel/plugin-proposal-class-properties',
            '@babel/plugin-proposal-object-rest-spread'
          ],
        }
      }
    }],
  }
}

四、css預處理

css可以使用sass/less或者其他你喜歡的處理器來處理,這裏以sass爲例:

先添加依賴:

npm i sass-loader node-sass style-loader css-loader -D

修改webpack配置:

{
  module: {
    rules: [{
      test: /\.scss$/,
      use: [{
        loader: 'style-loader',
      }, {
        loader: 'css-loader',
      }, {
        loader: 'sass-loader',
      }],
    }],
  }
}

五、靜態資源處理

除了css與js,咱們還需要處理一下圖片,不然可能會出現圖片404的現象。這裏使用url-loader和html-loader來處理css與html中的圖片。url-loader還可以將一些小圖片轉化成base64內聯在html中,這樣可以減少很多小圖片的請求。

使用html-loader是因爲,html中的圖片路徑如果不加處理,webpack會把它當做字符串從而忽略掉了這個文件,使用html-loader可以將圖片的src轉化爲require加載,從而被webpack捕獲,最終被url-loader處理。

<img src="./image.png" />

變成使用require加載,再賦給圖片地址,以下爲示意,只說明原理。

<img src="<%require('./image.png')%>" />

先添加依賴:

npm i url-loader html-loader -D

修改webpack配置:

{
  module: {
    rules: [{
      test: /\.(png|jpg|gif|jpeg|webp|svg|bmp|eot|ttf|woff)$/,
      use: [{
        loader: 'url-loader',
        options: {
          limit: 8192,
          name:'assets/images/[name]-[hash:8].[ext]',
        }
      }]
    }, {
      test: /\.(html)$/,
      use: {
        loader: 'html-loader',
        options: {
          attrs: [':data-src', ':src'],
        }
      },
    }],
  }
}

六、自動插入js文件

這裏使用的是靜態文件開發模式,最終生成的文件是應該是一個html加一個js文件的,開發的時候是一個html模板加一個ts文件,最終dist目錄下也應該有這個模板文件,這裏需要html-webpack-plugin插件來將模板文件複製到dist目錄,並且將生成的js文件插入到模板中。

npm i html-webpack-plugin -D

這是一個插件,一次調用只能處理一個入口文件,這裏是多個入口,所以需要修改一下上面的入口文件遍歷。

readDir(pageRoot).forEach(dir => {
  const entry = path.resolve(dir, `index.${entryScriptExt}`);
  if (isFileExistAsync(entry)) {
    const { name } = path.parse(dir);
    const page = path.resolve(dir, 'index.html');
    const htmlWebpackPluginConfig = {
      filename: `${name}.html`,
      chunks: [name],
      minify: !isDev && {
        removeAttributeQuotes:true,
        removeComments: true,
        collapseWhitespace: true,
        removeScriptTypeAttributes:true,
        removeStyleLinkTypeAttributes:true
      },
    };
    if (isFileExistAsync(page)) {
      htmlWebpackPluginConfig.template = page;
    }
    config.entry[name] = entry;
    config.plugins.push(new HtmlWebpackPlugin(htmlWebpackPluginConfig));
  }
});

每個入口都實例化一個插件實例,如果沒有提供模板文件,則使用默認的模板文件。

七、開發服務器

開發的時候還需要配合webpack-dev-server,這樣開發的時候可以啓動一個開發服務,並且可以實時編譯和熱替換,開發利器~~

npm i webpack-dev-server cross-env -D

開發服務器應該只在開發環境下使用,線上環境是不需要的,通過判斷NODE_ENV來確定是不是開發環境,只在開發環境下添加相關配置。

const isDev = process.env.NODE_ENV === 'development';
isDev && (config.devServer = {
  contentBase: path.join(__dirname, 'dist'),
  compress: true,
  hot: true,
  port: 10086,
});

再看一下啓動命令

"scripts": {
  "dev": "cross-env NODE_ENV=development webpack-dev-server --config ./build/webpack.config.js",
  "prod": "cross-env NODE_ENV=production webpack --config ./build/webpack.config.js --mode=production"
}

完美,一個小型的多頁webpack項目就搭建完成了,看一下項目結構。

結構.png

完整webpack配置文件如下

const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
​
const isDev = process.env.NODE_ENV === 'development';
const pageRoot = path.resolve(__dirname, '../src/pages');
const dist = path.resolve(__dirname, '../dist');
const entryScriptExt = 'ts';// 入口文件是ts文件
​
const config = {
  devtool: isDev ? 'inline-source-map' : 'none',
  mode: 'development',
  entry: {},
  output: {
    filename: 'assets/js/[name].[hash:8].js',
    path: dist,
  },
  resolve: {
    extensions: ['.tsx', '.js', '.jsx'],
  },
  module: {
    rules: [{
      test: /\.(ts|js)?$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            '@babel/preset-env',
            '@babel/preset-typescript',
          ],
          plugins: [
            '@babel/plugin-proposal-class-properties',
            '@babel/plugin-proposal-object-rest-spread'
          ],
        }
      }
    }, {
      test: /\.scss$/,
      use: [{
        loader: 'style-loader',
      }, {
        loader: 'css-loader',
      }, {
        loader: 'sass-loader',
      }],
    }, {
      test: /\.(png|jpg|gif|jpeg|webp|svg|bmp|eot|ttf|woff)$/,
      use: [{
        loader: 'url-loader',
        options: {
          limit: 8192,
          name:'assets/images/[name]-[hash:8].[ext]',
        }
      }]
    }, {
      test: /\.(html)$/,
      use: {
        loader: 'html-loader',
        options: {
          attrs: [':data-src', ':src'],
        }
      },
    }, ],
  },
  plugins: [
    new CleanWebpackPlugin(),
  ],
};
​
isDev && (config.devServer = {
  contentBase: path.join(__dirname, 'dist'),
  compress: true,
  hot: true,
  port: 10086,
});
​
readDir(pageRoot).forEach(dir => {
  const entry = path.resolve(dir, index.${entryScriptExt});
  if (isFileExistAsync(entry)) {
    const { name } = path.parse(dir);
    const page = path.resolve(dir, 'index.html');
    const htmlWebpackPluginConfig = {
      filename: `${name}.html`,
      chunks: [name],
      minify: !isDev && {
        removeAttributeQuotes:true,
        removeComments: true,
        collapseWhitespace: true,
        removeScriptTypeAttributes:true,
        removeStyleLinkTypeAttributes:true
      },
    };
    if (isFileExistAsync(page)) {
      htmlWebpackPluginConfig.template = page;
    }
    config.entry[name] = entry;
    config.plugins.push(new HtmlWebpackPlugin(htmlWebpackPluginConfig));
  }
});
​
/**
 * 同步判斷文件是否存在
 * 
 * @param {string} file 文件地址
 */
function isFileExistAsync(file) {
  try {
    fs.accessSync(file, fs.constants.F_OK);
  } catch (err) {
    return false;
  }
  return true;
}
​
/**
 * 遍歷某個文件夾,找出該文件夾下的所有一級子文件夾
 * 
 * @param {string} dir 文件目錄地址
 */
function readDir(dir) {
  let res = [];
  const list = fs.readdirSync(dir);
  list.forEach(file => {
    const pageDir = path.resolve(dir, file);
    const info = fs.statSync(pageDir);  
    if(info.isDirectory()){
      res.push(pageDir);
      // 暫時不要多層嵌套
      // res = [...res, ...readDir(pageDir)];
    }
  });
  return res;
}
​
module.exports = config;

完整項目源代碼放在github上,可以直接當做腳手架使用,如果此項目對您有幫助的話,歡迎不吝star~~~

本文首發於本人公衆號,前端小白菜,分享與關注前端技術,歡迎關注~~

前端小白菜

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章