奇舞-浅谈前端工程化
什么是’前端工程化’用工程化思想和工具来系统地解决前端开发过程中遇到通用问题,从而提升开发效率,增加代码的可扩展性和可维护性‘前端工程化’ 解决什么问题多人协作问题(规范,流程, 模块, 版本管理)效率问题(自动化工具)质量优化(单元测试,集成测试)性能优化(合并部署工具)安全性(转码,防御, xss 等等)……如何’前端工程化’确定开发调试的项目目录结构规划和部署上线的产品目录结
·
什么是’前端工程化’
用工程化思想和工具来系统地解决前端开发过程中遇到通用问题,从而提升开发效率,增加代码的可扩展性和可维护性
‘前端工程化’ 解决什么问题
- 多人协作问题(规范,流程, 模块, 版本管理)
- 效率问题(自动化工具)
- 质量优化(单元测试,集成测试)
- 性能优化(合并部署工具)
- 安全性(转码,防御, xss 等等)
- …
如何’前端工程化’
- 确定开发调试的项目目录结构
- 规划和部署上线的产品目录结构
- 根据开发环境开设计工作流
- 工具选择
- 撰写流程脚本
- 测试流程脚本
- 整理文档和其它工具
项目和产品发布的目录结构
建立开发环境与工作流
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
- 将图片等静态资源上传到CDN
- 替换css和js的路径
- 将css和js上传到CDN
- 替换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!');
});
其它工具
- grunt
- gulp
- baidu fis
- stc
- …
总结
这一节我们学习了
- 前端工程化的概念以及这么做的原因
- 前端工程化的流程工具 npm script 和webpack
- 周边其它工具和脚本的撰写
作业
为自己的小网站写工程化脚本
处理html, css, js和静态资源
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
已为社区贡献1条内容
所有评论(0)